diff --git a/apps/apollo-vertex/locales/en.json b/apps/apollo-vertex/locales/en.json index fc8a0541a..bcd56dc96 100644 --- a/apps/apollo-vertex/locales/en.json +++ b/apps/apollo-vertex/locales/en.json @@ -3,10 +3,21 @@ "access_denied": "Access Denied", "access_denied_description": "You don't have access to this application. Ask your administrator to add you to the users group.", "actions": "Actions", + "actual_input": "Actual input", + "actual_output": "Actual output", + "actual_version": "Actual version", "add_attachment": "Add attachment", + "add_to_expected_result": "Add to expected results", + "agent_adopted_successfully": "Agent added to expected results", + "agent_has_no_expected_output": "Agent has no expected output", + "agent_name": "Agent name", + "agent_produced_no_output": "Agent produced no output", + "agent_removed_from_expected_results": "Agent removed from expected results", + "agents_passed": "Agents passed", "ai_assistant": "AI Assistant", "ai_response_disclaimer": "AI-generated responses should be reviewed for accuracy.", "all": "All", + "all_tests_triggered": "All tests triggered", "analytics": "Analytics", "apollo_vertex": "Apollo Vertex", "apollo_vertex_description": "- UiPath Apollo Vertex", @@ -15,6 +26,8 @@ "assistant": "Assistant", "autopilot_empty_description": "Ask me anything about your automation data.", "bad_response": "Bad response", + "baseline_updated_successfully": "Expected result updated", + "baseline_version": "Baseline version", "business_user": "Business user", "cancel": "Cancel", "card_component": "Card Component", @@ -28,7 +41,10 @@ "click_me": "Click me", "close": "Close", "close_sidebar": "Close sidebar", + "collapse": "Collapse", "collapse_tool_calls": "Collapse tool calls", + "confirm_delete_test": "Delete this test?", + "confirm_delete_test_description": "This permanently removes the test and its baselines. This action cannot be undone.", "copied": "Copied!", "copy": "Copy", "copy_code": "Copy code", @@ -36,20 +52,37 @@ "copy_conversation_failed": "Couldn't copy conversation", "copy_failed": "Couldn't copy", "copy_payment_id": "Copy payment ID", + "current_baseline": "Current baseline", "custom": "Custom", "dark": "Dark", "dark_mode": "Dark Mode", "dashboard": "Dashboard", + "delete": "Delete", "desc": "Desc", "destructive": "Destructive", + "details": "Details", + "disabled": "Disabled", "displays_mobile_sidebar": "Displays the mobile sidebar.", "edit": "Edit", + "enabled": "Enabled", "english": "English", "enter_full_screen": "Enter full screen", "error": "Error", "exit_full_screen": "Exit full screen", + "expand": "Expand", "expand_tool_calls": "Expand tool calls", + "expected_input": "Expected input", + "expected_output": "Expected output", "export": "Export", + "failed_to_adopt_agent": "Failed to add agent to expected results", + "failed_to_delete_test": "Failed to delete test", + "failed_to_force_stop_batch": "Failed to force stop run batch", + "failed_to_force_stop_run": "Failed to force stop run", + "failed_to_load_run_details": "Failed to load run details", + "failed_to_remove_agent_baseline": "Failed to remove agent from expected results", + "failed_to_run_test": "Failed to run test", + "failed_to_run_tests": "Failed to run tests", + "failed_to_update_baseline": "Failed to update expected result", "feedback_comment_placeholder": "What could the AI have done better?", "feedback_demo_default_label": "Default — buttons only", "feedback_demo_disabled_label": "Disabled", @@ -57,6 +90,8 @@ "feedback_demo_with_comment_label": "With comment textarea", "feedback_vote_down": "Vote down", "feedback_vote_up": "Vote up", + "force_stop": "Force stop", + "force_stop_initiated": "Force stop initiated", "french": "French", "german": "German", "go_to_first_page": "Go to first page", @@ -68,10 +103,12 @@ "how_can_i_help_you": "How can I help you?", "image_preview": "Image preview", "import": "Import", + "in_baseline": "In baseline", "info": "Info", "japanese": "Japanese", "korean": "Korean", "language": "Language", + "last_run_score": "Last run score", "light": "Light", "light_mode": "Light Mode", "load_from_preset": "Load from preset:", @@ -88,14 +125,19 @@ "new_conversation_confirm_title": "Start a new conversation?", "next": "Next", "next_slide": "Next slide", + "no_agents_configured": "No agents configured", "no_countable_in_entity": "{{entity}} has no field that can be counted.", "no_countable_in_joined": "No countable field is available on {{primary}} or the joined entities.", + "no_data_available": "No data available", "no_frameworks_found": "No frameworks found.", "no_results": "No results.", + "no_results_available": "No results available", "of": "of", "open_menu": "Open menu", "open_sidebar": "Open sidebar", "outline": "Outline", + "output_evaluator_results": "Output evaluator results", + "overall_score": "Overall score", "page": "Page", "pick_a_date": "Pick a date", "portuguese": "Portuguese", @@ -107,14 +149,23 @@ "primary_button": "Primary Button", "projects": "Projects", "remove_file": "Remove {{name}}", + "remove_from_expected_result": "Remove from expected results", "remove_quoted_text": "Remove quoted text", + "removed": "Removed", "reset_to_default": "Reset to Default", "retry": "Retry", "romanian": "Romanian", "rows_per_page": "Rows per page", "rows_selected": "{{selected}} of {{total}} row(s) selected.", + "run": "Run", + "run_all": "Run all", + "run_date": "Run date", + "run_results": "Run results", + "runs_in_progress": "Runs in progress", "russian": "Russian", "save_and_rerun": "Save & re-run", + "score": "Score", + "score_trend": "Score trend", "scroll_to_bottom": "Scroll to bottom", "search": "Search...", "search_frameworks": "Search frameworks", @@ -131,20 +182,35 @@ "sign_in_with_uipath": "Sign in with UiPath", "sign_out": "Sign out", "signed_in_as": "Signed in as {{email}}", + "solution_tests": "Solution tests", "sort_by_column": "Sort by {{column}}", "sort_by_column_sorted_ascending": "Sort by {{column}} (sorted ascending)", "sort_by_column_sorted_descending": "Sort by {{column}} (sorted descending)", "spanish": "Spanish", "spanish-mx": "Spanish (Mexico)", "start_conversation_with": "Start a conversation with {{name}}", + "status": "Status", "stop": "Stop", + "subject_passed": "{{subject}} passed", + "subject_passing": "{{subject}} passing", + "subject_run_results": "{{subject}} {{id}}", "success": "Success", "system": "System", "table_no_valid_fields": "None of the requested fields exist on {{entity}}: {{fields}}.", "team": "Team", + "test_cases": "Test cases", + "test_creation_failed": "Test creation failed", + "test_deleted": "Test deleted", + "test_disabled": "Test disabled", + "test_enabled": "Test enabled", + "test_name": "Test name", + "test_runs": "Test runs", + "test_triggered": "Test triggered", + "tests_passed": "Tests passed", + "tests_passing": "Tests passing", "theme_editor": "Theme Editor", - "thinking": "Thinking", "theme_preview_description": "This is a preview of the card component with your custom theme.", + "thinking": "Thinking", "toggle_columns": "Toggle columns", "toggle_sidebar": "Toggle Sidebar", "toggle_theme": "Toggle theme", @@ -158,12 +224,16 @@ "unknown_entity": "Entity \"{{entity}}\" doesn't exist.", "unknown_field_in_entity": "Field \"{{field}}\" doesn't exist on {{entity}}.", "unknown_field_in_joined": "Field \"{{field}}\" doesn't exist on the joined entities.", + "update_expected_result": "Update expected result", "upload_files": "Upload files", "user_email_placeholder": "user@company.com", "user_profile": "Profile and settings", "verifying_access": "Verifying access…", + "version": "Version", + "version_differs_from_baseline": "Version differs from baseline", "view": "View", "view_customer": "View customer", + "view_expected": "View expected", "view_payment_details": "View payment details", "warning": "Warning", "wrong_dimension_type_in_entity": "Field \"{{field}}\" on {{entity}} is {{actual}}; this chart needs a {{expected}} field.", diff --git a/apps/apollo-vertex/registry/solution-tests/actions.ts b/apps/apollo-vertex/registry/solution-tests/actions.ts new file mode 100644 index 000000000..7536fde52 --- /dev/null +++ b/apps/apollo-vertex/registry/solution-tests/actions.ts @@ -0,0 +1,52 @@ +/** + * The Solution Tests write surface. The smart container resolves these from + * the `SolutionTestsProvider`'s `actions` prop; supply your own implementation, + * or use `createSolutionTestActions` for the standard Solution Test framework + * contract (see `create-actions`). + */ + +import { notImplemented } from "./mutations"; +import type { MutationResult, ResultAttachmentField } from "./types"; + +export interface SolutionTestsActions { + /** Run the given tests, or all active Ready tests when omitted. */ + runTests(testIds?: string[]): Promise; + /** Toggle a test's active flag. */ + toggleTestActive(testId: string, isActive: boolean): Promise; + /** Delete a test. */ + deleteTest(testId: string): Promise; + /** Force-stop every active run in a batch. */ + forceStopBatch(batchId: string): Promise; + /** Force-stop a single run. */ + forceStopRun(runId: string): Promise; + /** Adopt a run result's job as the test baseline. */ + adoptJob(runResultId: string): Promise; + /** Update the test baseline from a run result. */ + updateBaseline(runResultId: string): Promise; + /** Remove a job from the test's expected (baseline) results. */ + removeJobBaseline(jobBaselineId: string): Promise; + /** Expected-output attachment for a baseline job (Agents expansion). */ + getJobExpectedOutput(jobId: string): Promise; + /** One attachment slot for a run result (run-details expansion). */ + getResultAttachment( + resultId: string, + field: ResultAttachmentField, + ): Promise; +} + +/** + * Fallback used when the provider is given no `actions` — every method throws + * *when invoked* (not at hook-call time, so smart containers still render). + */ +export const notImplementedActions: SolutionTestsActions = { + runTests: () => notImplemented("runTests"), + toggleTestActive: () => notImplemented("toggleTestActive"), + deleteTest: () => notImplemented("deleteTest"), + forceStopBatch: () => notImplemented("forceStopBatch"), + forceStopRun: () => notImplemented("forceStopRun"), + adoptJob: () => notImplemented("adoptJob"), + updateBaseline: () => notImplemented("updateBaseline"), + removeJobBaseline: () => notImplemented("removeJobBaseline"), + getJobExpectedOutput: () => notImplemented("getJobExpectedOutput"), + getResultAttachment: () => notImplemented("getResultAttachment"), +}; diff --git a/apps/apollo-vertex/registry/solution-tests/collections.ts b/apps/apollo-vertex/registry/solution-tests/collections.ts new file mode 100644 index 000000000..6ae32ef08 --- /dev/null +++ b/apps/apollo-vertex/registry/solution-tests/collections.ts @@ -0,0 +1,41 @@ +import type { Collection } from "@tanstack/react-db"; + +/** + * Well-known Solution Test entity collection names. The collections live on the + * dedicated, name-keyed `solutionTests` namespace of the vs-core solution + * (`solution.api.collections.solutionTests[NAME]`, added in @uipath/vs-core 2.x). + */ +export const ENTITY = { + tests: "UiPathSTTests", + batchRuns: "UiPathSTBatchRuns", + runs: "UiPathSTRuns", + jobs: "UiPathSTJobs", + runResults: "UiPathSTRunResults", +} as const; + +/** Structural subset of the vs-core solution we touch — keeps this file free + * of a hard `@uipath/vs-core` type dependency. */ +interface SolutionLike { + api: { collections: { solutionTests: unknown } }; +} + +/** + * Resolve a Solution Test collection by its well-known name and bridge it to the + * row type the view expects. + * + * vs-core types the `solutionTests` namespace by the *consumer's* entity + * generic (`Partial>`), which a domain-neutral component + * can't supply — so the map isn't statically indexable to a known row type. + * This is the single, deliberate boundary where we assert the row shape; the + * read hooks stay assertion-free and feed the result straight to `useLiveQuery`. + */ +export function solutionTestCollection( + solution: SolutionLike | null, + name: string, +): Collection | undefined { + // oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- the one deliberate boundary: vs-core types this map by the consumer's entity generic, unknowable to a domain-neutral component + const map = solution?.api.collections.solutionTests as + | Record | undefined> + | undefined; + return map?.[name]; +} diff --git a/apps/apollo-vertex/registry/solution-tests/config.ts b/apps/apollo-vertex/registry/solution-tests/config.ts new file mode 100644 index 000000000..1f31c0cd1 --- /dev/null +++ b/apps/apollo-vertex/registry/solution-tests/config.ts @@ -0,0 +1,75 @@ +/** + * Presentation config for the Solution Tests view. + * + * The view fetches generic Solution Test entities; everything that varies by + * vertical is parameterized here. In practice the only required customization + * is `subjectColumns` (the vertical's own columns). Framework strings are + * translated with i18n inside the components. + */ + +import type { ColumnDef } from "@tanstack/react-table"; +import type { SolutionTest } from "./types"; + +/** Optional override maps for the numeric status enums (e.g. localized labels). */ +export interface StatusLabelOverrides { + test?: Record; + run?: Record; + runResult?: Record; +} + +export interface SolutionTestsConfig { + /** Extra subject columns inserted between the Test Name and Version columns. */ + subjectColumns?: ColumnDef[]; + /** Navigation href for a test's subject; when set, the test name is a link. */ + getSubjectHref?: (test: SolutionTest) => string | undefined; + /** + * The subject entity's noun, e.g. `{ singular: "Loan", plural: "Loans" }`. + * Drives the "{subject} passing" KPI card, the "{subject} passed" run column, + * and the run-results dialog title. When omitted, generic "Tests" wording is + * used. Both forms are interpolated into translated strings. + */ + subjectNoun?: { singular: string; plural: string }; + /** Maps evaluator ids to display labels in the evaluator-results panel. */ + evaluatorLabels?: Record; + /** Override the numeric status enum labels (defaults are English fallbacks). */ + statusLabels?: StatusLabelOverrides; + /** Score >= threshold renders as a pass; also the KPI trend line. Default 0.9. */ + passThreshold?: number; + /** Polling cadence while runs are active, in ms. Default 5000. */ + pollIntervalMs?: number; + /** Show the expected/actual input panels in run-result details. Default false. */ + showInputs?: boolean; +} + +/** Config with all defaults applied — what components consume from context. */ +export interface ResolvedSolutionTestsConfig + extends Required< + Pick< + SolutionTestsConfig, + "passThreshold" | "pollIntervalMs" | "showInputs" | "evaluatorLabels" + > + > { + subjectColumns: ColumnDef[]; + getSubjectHref?: (test: SolutionTest) => string | undefined; + subjectNoun?: { singular: string; plural: string }; + statusLabels: Required; +} + +export function resolveConfig( + config: SolutionTestsConfig = {}, +): ResolvedSolutionTestsConfig { + return { + subjectColumns: config.subjectColumns ?? [], + getSubjectHref: config.getSubjectHref, + subjectNoun: config.subjectNoun, + evaluatorLabels: config.evaluatorLabels ?? {}, + statusLabels: { + test: config.statusLabels?.test ?? {}, + run: config.statusLabels?.run ?? {}, + runResult: config.statusLabels?.runResult ?? {}, + }, + passThreshold: config.passThreshold ?? 0.9, + pollIntervalMs: config.pollIntervalMs ?? 5000, + showInputs: config.showInputs ?? false, + }; +} diff --git a/apps/apollo-vertex/registry/solution-tests/context.tsx b/apps/apollo-vertex/registry/solution-tests/context.tsx new file mode 100644 index 000000000..11cb28c05 --- /dev/null +++ b/apps/apollo-vertex/registry/solution-tests/context.tsx @@ -0,0 +1,73 @@ +"use client"; + +/** + * Carries the Solution Tests view's presentation `config` and its write + * `actions`. Deeply-nested components (expanded rows, dialogs) read config from + * here; the write/attachment hooks resolve their `actions` from here. Reads + * come from the collection-backed query hooks, so the whole tree stays portable. + */ + +import { createContext, useContext, useMemo, type ReactNode } from "react"; +import { notImplementedActions, type SolutionTestsActions } from "./actions"; +import { + resolveConfig, + type ResolvedSolutionTestsConfig, + type SolutionTestsConfig, +} from "./config"; + +interface SolutionTestsContextValue { + config: ResolvedSolutionTestsConfig; + actions: SolutionTestsActions; +} + +const SolutionTestsContext = createContext( + null, +); + +interface SolutionTestsProviderProps { + config?: SolutionTestsConfig; + /** + * Write/attachment implementations. Omit for a read-only view (invoking a + * write then throws). Use `createSolutionTestActions` for the standard + * Solution Test framework contract, or supply your own. + */ + actions?: SolutionTestsActions; + children: ReactNode; +} + +export const SolutionTestsProvider: React.FC = ({ + config, + actions, + children, +}) => { + const value = useMemo( + () => ({ + config: resolveConfig(config), + actions: actions ?? notImplementedActions, + }), + [config, actions], + ); + return ( + + {children} + + ); +}; + +export function useSolutionTestsContext(): SolutionTestsContextValue { + const ctx = useContext(SolutionTestsContext); + if (!ctx) { + throw new Error( + "useSolutionTestsContext must be used within a SolutionTestsProvider", + ); + } + return ctx; +} + +export function useSolutionTestsConfig(): ResolvedSolutionTestsConfig { + return useSolutionTestsContext().config; +} + +export function useSolutionTestsActions(): SolutionTestsActions { + return useSolutionTestsContext().actions; +} diff --git a/apps/apollo-vertex/registry/solution-tests/create-actions.ts b/apps/apollo-vertex/registry/solution-tests/create-actions.ts new file mode 100644 index 000000000..468f1f46e --- /dev/null +++ b/apps/apollo-vertex/registry/solution-tests/create-actions.ts @@ -0,0 +1,221 @@ +"use client"; + +/** + * Default `SolutionTestsActions` for the standard UiPath Solution Test + * framework contract — so a consumer wires a handful of values instead of + * reimplementing the whole write service: + * + * - **run** starts the `UiPathSolutionTestOrchestratorAgent` via its + * `run_solution_tests` API trigger; + * - **delete / adopt / baseline / force-stop** call the `automation-functions` + * API trigger (an RPC router) with a `/solution-tests/*` path; + * - **toggle-active** and **attachment reads** go through the consumer's + * entity data access (`updateEntity` / `getAttachment`). + * + * Trigger URL: `{baseUrl}/{orgName}/{tenantName}/orchestrator_/t/{folderKey}/{slug}` + * (relative by default → same-origin). The caller's bearer token authenticates + * the POST and is also injected into the body for the backend's RBAC checks. + * + * A consumer whose backend deviates from this contract can pass its own + * `SolutionTestsActions` to the provider instead. + */ + +import type { SolutionTestsActions } from "./actions"; +import { ENTITY } from "./collections"; +import type { MutationResult, ResultAttachmentField } from "./types"; + +export interface SolutionTestActionDeps { + /** Org name segment of the trigger URL (e.g. "vertical_solution_fins"). */ + orgName: string; + /** Tenant name segment (e.g. "LOAN_ORIGINATION_POC_DEV"). */ + tenantName: string; + /** Solution folder key — the `/t/{folderKey}/` segment. */ + folderKey: string; + /** Resolve the caller's current bearer token (refreshed near expiry). */ + getToken: () => Promise | string | null; + /** Origin for the trigger URL. Defaults to "" (same-origin / relative). */ + baseUrl?: string; + /** Persist an entity field change — used by `toggleTestActive`. */ + updateEntity: ( + entityName: string, + recordId: string, + patch: Record, + ) => Promise; + /** Read an attachment field off an entity record. */ + getAttachment: ( + entityName: string, + recordId: string, + field: string, + ) => Promise; +} + +const RUN_SLUG = "run_solution_tests"; +const FN_SLUG = "automation-functions"; +const AGENT_SUCCESS = new Set(["success", "partial_success"]); +const ALREADY_RUNNING = "already_running"; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +/** The agent wraps its outcome in `output_data.status`; the RPC router doesn't. */ +function agentStatus(data: unknown): string | null { + if (!isRecord(data)) return null; + const output = isRecord(data.output_data) ? data.output_data : data; + return typeof output.status === "string" ? output.status : null; +} + +/** + * Detect an app-level failure across both backend contracts. Orchestrator + * returns HTTP 200 even on RBAC denials, so the real outcome is in the body: + * the RPC router reports `status_code >= 400`; the agent reports a non-success + * `output_data.status`. + */ +function detectFailure(data: unknown): string | null { + if (!isRecord(data)) return null; + if (typeof data.status_code === "number" && data.status_code >= 400) { + return typeof data.error === "string" ? data.error : "request_failed"; + } + const status = agentStatus(data); + if (status != null && !AGENT_SUCCESS.has(status)) { + const output = isRecord(data.output_data) ? data.output_data : data; + return typeof output.message === "string" ? output.message : status; + } + return null; +} + +export function createSolutionTestActions( + deps: SolutionTestActionDeps, +): SolutionTestsActions { + const { + orgName, + tenantName, + folderKey, + getToken, + baseUrl = "", + updateEntity, + getAttachment, + } = deps; + + function triggerUrl(slug: string): string { + return `${baseUrl}/${orgName}/${tenantName}/orchestrator_/t/${folderKey}/${slug}`; + } + + async function invokeTrigger( + slug: string, + buildBody: (token: string) => Record, + ): Promise { + const token = await getToken(); + if (!token) { + return { success: false, message: "No access token available." }; + } + try { + const response = await fetch(triggerUrl(slug), { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(buildBody(token)), + }); + if (!response.ok) { + const text = await response.text(); + return { + success: false, + message: `API trigger returned ${response.status}: ${text.slice(0, 300)}`, + }; + } + const data: unknown = response.headers + .get("content-type") + ?.includes("application/json") + ? await response.json().catch(() => null) + : null; + const failure = detectFailure(data); + if (failure) return { success: false, message: failure, data }; + return { success: true, data }; + } catch { + // Network/timeout — the job was likely started; treat as fire-and-forget. + return { + success: true, + message: "Request sent (response timed out — job may still be running)", + }; + } + } + + /** + * Call the `automation-functions` RPC router. The caller's token goes in the + * body's `_auth.userToken` envelope (the router parses the body itself). + */ + function callFn( + path: string, + body: Record, + ): Promise { + return invokeTrigger(FN_SLUG, (token) => ({ + method: "POST", + path, + body: { ...body, _auth: { userToken: token } }, + })); + } + + return { + async runTests(testIds?: string[]): Promise { + // The agent maps trigger input onto GraphInput by field name, so the + // token goes in a TOP-LEVEL `auth` field here (not `_auth`). + const result = await invokeTrigger(RUN_SLUG, (token) => ({ + ...(testIds ? { solution_test_ids: testIds } : {}), + auth: { userToken: token }, + })); + // `already_running` is a non-error rejection — surface it distinctly. + if (agentStatus(result.data) === ALREADY_RUNNING) { + return { success: false, message: ALREADY_RUNNING }; + } + return { success: result.success, message: result.message }; + }, + + async toggleTestActive(testId: string, isActive: boolean): Promise { + await updateEntity(ENTITY.tests, testId, { IsActive: isActive }); + }, + + deleteTest(testId: string): Promise { + return callFn("/solution-tests/delete", { solution_test_id: testId }); + }, + + forceStopBatch(batchId: string): Promise { + return callFn("/solution-tests/force-stop-batch", { batch_id: batchId }); + }, + + forceStopRun(runId: string): Promise { + return callFn("/solution-tests/force-stop", { run_id: runId }); + }, + + adoptJob(runResultId: string): Promise { + return callFn("/solution-tests/adopt-job", { + run_result_id: runResultId, + }); + }, + + updateBaseline(runResultId: string): Promise { + return callFn("/solution-tests/update-baseline", { + run_result_id: runResultId, + }); + }, + + removeJobBaseline(jobBaselineId: string): Promise { + return callFn("/solution-tests/remove-job-baseline", { + job_baseline_id: jobBaselineId, + }); + }, + + getJobExpectedOutput(jobId: string): Promise { + return getAttachment(ENTITY.jobs, jobId, "ExpectedOutput"); + }, + + getResultAttachment( + resultId: string, + field: ResultAttachmentField, + ): Promise { + return getAttachment(ENTITY.runResults, resultId, field); + }, + }; +} diff --git a/apps/apollo-vertex/registry/solution-tests/hooks.ts b/apps/apollo-vertex/registry/solution-tests/hooks.ts new file mode 100644 index 000000000..b5918d5de --- /dev/null +++ b/apps/apollo-vertex/registry/solution-tests/hooks.ts @@ -0,0 +1,49 @@ +/** + * Collection-backed data layer for the Solution Tests view — one module per + * query entity, each owning its live read plus the writes that act on it: + * - `use-solution-tests` — tests list + run / toggle-active / delete + * - `use-solution-test-batch-runs` — batch-run list + * - `use-solution-test-runs` — run list + * - `use-baseline-jobs` — baseline jobs + remove-baseline / expected-output + * - `use-run-results` — run results + adopt / update-baseline / attachment + * - `use-force-stop` — force-stop a batch or a single run + * + * Reads are live (one `useSolution()` + `useLiveQuery` per collection); the + * view re-renders as the collections change. The write / attachment hooks are + * placeholders until the vs-core solutionTests write surface lands — each + * returns a callable that throws when invoked, so smart containers still render. + */ + +export { + useSolutionTests, + useRunTests, + useToggleTestActive, + useDeleteTest, +} from "./use-solution-tests"; +export type { UseSolutionTestsResult } from "./use-solution-tests"; + +export { useSolutionTestBatchRuns } from "./use-solution-test-batch-runs"; +export type { UseSolutionTestBatchRunsResult } from "./use-solution-test-batch-runs"; + +export { useSolutionTestRuns } from "./use-solution-test-runs"; +export type { UseSolutionTestRunsResult } from "./use-solution-test-runs"; + +export { + useBaselineJobs, + useRemoveJobBaseline, + useJobExpectedOutput, +} from "./use-baseline-jobs"; +export type { UseBaselineJobsResult } from "./use-baseline-jobs"; + +export { + useRunResults, + useAdoptJob, + useUpdateBaseline, + useResultAttachment, +} from "./use-run-results"; +export type { UseRunResultsResult } from "./use-run-results"; + +export { useForceStopBatch, useForceStopRun } from "./use-force-stop"; + +export type { MutationHookResult, AttachmentFetcher } from "./mutations"; +export type { MutationResult, ResultAttachmentField } from "./types"; diff --git a/apps/apollo-vertex/registry/solution-tests/mutations.ts b/apps/apollo-vertex/registry/solution-tests/mutations.ts new file mode 100644 index 000000000..6229bc1f7 --- /dev/null +++ b/apps/apollo-vertex/registry/solution-tests/mutations.ts @@ -0,0 +1,39 @@ +/** + * Shared primitives for the Solution Tests write hooks. + * + * vs-core exposes no write/attachment surface for solution tests yet, so each + * action hook returns a callable that throws *when invoked* (not at hook-call + * time, so smart containers still render) and reports `isPending: false`. Swap + * the `notImplemented(...)` bodies for the real vs-core calls once they land. + */ + +import type { MutationResult } from "./types"; + +export function notImplemented(action: string): never { + throw new Error( + `${action}: not implemented — provide \`actions\` to the SolutionTestsProvider`, + ); +} + +/** + * Await an action and convert an app-level `{ success: false }` into a thrown + * error, so the smart container's toast-on-reject handlers fire as expected. + */ +export async function runMutation( + action: () => Promise, +): Promise { + const result = await action(); + if (!result.success) { + throw new Error(result.message ?? "Action failed"); + } + return result; +} + +export interface MutationHookResult { + mutate: (...args: TArgs) => Promise; + isPending: boolean; +} + +export interface AttachmentFetcher { + fetch: (...args: TArgs) => Promise; +} diff --git a/apps/apollo-vertex/registry/solution-tests/status-maps.ts b/apps/apollo-vertex/registry/solution-tests/status-maps.ts new file mode 100644 index 000000000..9523de486 --- /dev/null +++ b/apps/apollo-vertex/registry/solution-tests/status-maps.ts @@ -0,0 +1,32 @@ +import { RunStatus, RunResultStatus, SolutionTestStatus } from "./types"; + +export type BadgeStatus = "success" | "warning" | "error" | "info"; + +export const testStatusBadgeMap: Record = { + [SolutionTestStatus.Ready]: "success", + [SolutionTestStatus.Pending]: "warning", + [SolutionTestStatus.Error]: "error", +}; + +export const runStatusBadgeMap: Record = { + [RunStatus.Passed]: "success", + [RunStatus.Running]: "warning", + [RunStatus.Failed]: "error", + [RunStatus.Error]: "error", + [RunStatus.Pending]: "info", + [RunStatus.Aborted]: "warning", +}; + +export const runResultStatusBadgeMap: Record = { + [RunResultStatus.Passed]: "success", + [RunResultStatus.Failed]: "error", + [RunResultStatus.Error]: "warning", + [RunResultStatus.NoBaseline]: "info", + [RunResultStatus.Pending]: "info", + [RunResultStatus.Aborted]: "warning", +}; + +export const resultBadgeClassMap: Record = { + [RunResultStatus.Missing]: + "border-transparent bg-muted text-muted-foreground", +}; diff --git a/apps/apollo-vertex/registry/solution-tests/types.ts b/apps/apollo-vertex/registry/solution-tests/types.ts new file mode 100644 index 000000000..111b411de --- /dev/null +++ b/apps/apollo-vertex/registry/solution-tests/types.ts @@ -0,0 +1,180 @@ +/** + * Solution Test view models — the data contracts this UI renders. + * + * These map to the UiPathST* family of Solution Test entities and are + * intentionally domain-neutral. The "subject" of a test (a loan, an invoice, + * a claim, …) is represented by the optional `SubjectId` / `Subject` fields; + * consumers map their own entity onto those via the adapter and render any + * subject-specific columns through `SolutionTestsConfig`. + */ + +// ============================================================================ +// Choice sets +// ============================================================================ + +export const SolutionTestStatus = { + Pending: 0, + Ready: 1, + Error: 2, +} as const; + +export type SolutionTestStatusValue = + (typeof SolutionTestStatus)[keyof typeof SolutionTestStatus]; + +export const RunStatus = { + Pending: 0, + Running: 1, + Passed: 2, + Failed: 3, + Error: 4, + Aborted: 5, +} as const; + +export type RunStatusValue = (typeof RunStatus)[keyof typeof RunStatus]; + +export const RunResultStatus = { + Pending: 0, + Passed: 1, + Failed: 2, + Error: 3, + NoBaseline: 4, + Missing: 5, + Aborted: 6, +} as const; + +export type RunResultStatusValue = + (typeof RunResultStatus)[keyof typeof RunResultStatus]; + +export const JobRole = { + EntryPoint: 0, + Baseline: 1, +} as const; + +export type JobRoleValue = (typeof JobRole)[keyof typeof JobRole]; + +// ============================================================================ +// Status label maps (English defaults; override via config.statusLabels) +// ============================================================================ + +export const defaultTestStatusLabels: Record = { + [SolutionTestStatus.Pending]: "Pending", + [SolutionTestStatus.Ready]: "Ready", + [SolutionTestStatus.Error]: "Error", +}; + +export const defaultRunStatusLabels: Record = { + [RunStatus.Pending]: "Pending", + [RunStatus.Running]: "Running", + [RunStatus.Passed]: "Passed", + [RunStatus.Failed]: "Failed", + [RunStatus.Error]: "Error", + [RunStatus.Aborted]: "Aborted", +}; + +export const defaultRunResultStatusLabels: Record = { + [RunResultStatus.Pending]: "Pending", + [RunResultStatus.Passed]: "Passed", + [RunResultStatus.Failed]: "Failed", + [RunResultStatus.Error]: "Error", + [RunResultStatus.NoBaseline]: "New", + [RunResultStatus.Missing]: "Did not run", + [RunResultStatus.Aborted]: "Aborted", +}; + +// ============================================================================ +// Entity interfaces +// ============================================================================ + +export interface SolutionTest { + Id: string; + TestName?: string; + VerticalSolutionVersion?: string; + Status: number; + IsActive?: boolean; + UserMessages?: string; + /** Business identifier of the test's subject, shown in the table + dialog. */ + SubjectId?: string; + /** Arbitrary subject fields, read by consumer-supplied subject columns. */ + Subject?: Record; +} + +export interface SolutionTestJob { + Id: string; + SolutionTestId: string; + JobRole: number; + ProcessName: string; + ProcessVersion?: string; + SourceRunResultId?: string; +} + +export interface SolutionTestRun { + Id: string; + SolutionTestId: string; + RunBatchId: string; + Status: number; + VerticalSolutionVersion?: string; + TestRunScore?: number; + JobsPassed?: number; + JobsTotal?: number; + UserMessages?: string; + StartedAt?: string; + CompletedAt?: string; + CreateTime?: string; +} + +export interface SolutionTestBatchRun { + Id: string; + Status: number; + OverallScore?: number; + TestsPassed?: number; + TestsTotal?: number; + VerticalSolutionVersion?: string; + UserMessages?: string; + StartedAt?: string; + CompletedAt?: string; + CreateTime?: string; +} + +export interface SolutionTestRunResult { + Id: string; + SolutionTestRunId: string; + /** The job this result belongs to; expanded to read JobRole when filtering. */ + SolutionTestJobId?: string | SolutionTestJob; + ProcessName: string; + ProcessVersion?: string; + BaselineProcessVersion?: string; + Status: number; + Score?: number; + UserMessages?: string; + ErrorMessage?: string; +} + +// ============================================================================ +// User messages +// ============================================================================ + +export interface UserMessageItem { + key: string; + category: "Error" | "Warning" | "Info"; + details: Record; + timestamp: string; + message: string; +} + +// ============================================================================ +// Write / attachment contracts +// ============================================================================ + +/** Outcome of a write operation. `message` is already human-readable text. */ +export interface MutationResult { + success: boolean; + message?: string; +} + +/** Per-result attachment slots surfaced in the run-details dialog. */ +export type ResultAttachmentField = + | "ExpectedOutput" + | "ExpectedInput" + | "ActualOutput" + | "ActualInput" + | "EvaluatorResults"; diff --git a/apps/apollo-vertex/registry/solution-tests/use-baseline-jobs.ts b/apps/apollo-vertex/registry/solution-tests/use-baseline-jobs.ts new file mode 100644 index 000000000..049896b32 --- /dev/null +++ b/apps/apollo-vertex/registry/solution-tests/use-baseline-jobs.ts @@ -0,0 +1,60 @@ +"use client"; + +/** + * Baseline jobs for a test: the live list plus the writes that act on jobs — + * remove-from-baseline and the expected-output attachment fetch (Agents + * expansion). Reads stream from the `UiPathSTJobs` collection; the writes are + * placeholders pending the vs-core write surface (see `mutations`). + */ + +import { useLiveQuery } from "@tanstack/react-db"; +import { useSolution } from "@uipath/vs-core"; +import { ENTITY, solutionTestCollection } from "./collections"; +import { useSolutionTestsActions } from "./context"; +import { + type AttachmentFetcher, + type MutationHookResult, + runMutation, +} from "./mutations"; +import { JobRole } from "./types"; +import type { MutationResult, SolutionTestJob } from "./types"; + +export interface UseBaselineJobsResult { + jobs: SolutionTestJob[]; + isLoading: boolean; +} + +/** Live baseline jobs for a test. */ +export function useBaselineJobs(testId: string): UseBaselineJobsResult { + const solution = useSolution(); + const collection = solutionTestCollection( + solution, + ENTITY.jobs, + ); + const { data, isLoading } = useLiveQuery(() => collection); + const jobs = (data ?? []).filter( + (job) => job.SolutionTestId === testId && job.JobRole === JobRole.Baseline, + ); + return { jobs, isLoading }; +} + +/** Remove a job from the test's expected (baseline) results. */ +export function useRemoveJobBaseline(): MutationHookResult< + [baselineId: string], + MutationResult +> { + const actions = useSolutionTestsActions(); + return { + mutate: (baselineId: string) => + runMutation(() => actions.removeJobBaseline(baselineId)), + isPending: false, + }; +} + +/** Expected-output attachment for a baseline job (Agents expansion). */ +export function useJobExpectedOutput(): AttachmentFetcher<[jobId: string]> { + const actions = useSolutionTestsActions(); + return { + fetch: (jobId: string) => actions.getJobExpectedOutput(jobId), + }; +} diff --git a/apps/apollo-vertex/registry/solution-tests/use-force-stop.ts b/apps/apollo-vertex/registry/solution-tests/use-force-stop.ts new file mode 100644 index 000000000..6003d1739 --- /dev/null +++ b/apps/apollo-vertex/registry/solution-tests/use-force-stop.ts @@ -0,0 +1,36 @@ +"use client"; + +/** + * Force-stop writes for in-flight work — one for a whole batch run, one for a + * single run. Grouped together as the stop surface; both resolve their + * implementation from the provider's `actions`. + */ + +import { useSolutionTestsActions } from "./context"; +import { type MutationHookResult, runMutation } from "./mutations"; +import type { MutationResult } from "./types"; + +/** Force-stop a whole batch run. */ +export function useForceStopBatch(): MutationHookResult< + [batchId: string], + MutationResult +> { + const actions = useSolutionTestsActions(); + return { + mutate: (batchId: string) => + runMutation(() => actions.forceStopBatch(batchId)), + isPending: false, + }; +} + +/** Force-stop a single run. */ +export function useForceStopRun(): MutationHookResult< + [runId: string], + MutationResult +> { + const actions = useSolutionTestsActions(); + return { + mutate: (runId: string) => runMutation(() => actions.forceStopRun(runId)), + isPending: false, + }; +} diff --git a/apps/apollo-vertex/registry/solution-tests/use-run-results.ts b/apps/apollo-vertex/registry/solution-tests/use-run-results.ts new file mode 100644 index 000000000..7d168f251 --- /dev/null +++ b/apps/apollo-vertex/registry/solution-tests/use-run-results.ts @@ -0,0 +1,90 @@ +"use client"; + +/** + * Run results for a run: the live list plus the writes that act on a result — + * adopt a job as baseline, update an existing baseline — and the per-result + * attachment fetch used by the run-details expansion. Reads stream from the + * `UiPathSTRunResults` collection; the writes are placeholders pending the + * vs-core write surface (see `mutations`). + */ + +import { useLiveQuery } from "@tanstack/react-db"; +import { useSolution } from "@uipath/vs-core"; +import { ENTITY, solutionTestCollection } from "./collections"; +import { useSolutionTestsActions } from "./context"; +import { + type AttachmentFetcher, + type MutationHookResult, + runMutation, +} from "./mutations"; +import { JobRole } from "./types"; +import type { + MutationResult, + ResultAttachmentField, + SolutionTestRunResult, +} from "./types"; + +export interface UseRunResultsResult { + results: SolutionTestRunResult[]; + isLoading: boolean; +} + +/** Resolve a result's job role, whether the job is embedded or a bare id. */ +function isBaselineEntryPoint(result: SolutionTestRunResult): boolean { + const job = result.SolutionTestJobId; + if (job != null && typeof job === "object") { + return job.JobRole === JobRole.Baseline; + } + // No expanded job to inspect — keep the row. + return true; +} + +/** Live run results for a run (drops non-baseline entry-point results). */ +export function useRunResults(runId: string): UseRunResultsResult { + const solution = useSolution(); + const collection = solutionTestCollection( + solution, + ENTITY.runResults, + ); + const { data, isLoading } = useLiveQuery(() => collection); + const results = (data ?? []) + .filter((result) => result.SolutionTestRunId === runId) + .filter((result) => isBaselineEntryPoint(result)); + return { results, isLoading }; +} + +/** Adopt a run result's job as the test baseline. */ +export function useAdoptJob(): MutationHookResult< + [resultId: string], + MutationResult +> { + const actions = useSolutionTestsActions(); + return { + mutate: (resultId: string) => runMutation(() => actions.adoptJob(resultId)), + isPending: false, + }; +} + +/** Update the test baseline from a run result. */ +export function useUpdateBaseline(): MutationHookResult< + [resultId: string], + MutationResult +> { + const actions = useSolutionTestsActions(); + return { + mutate: (resultId: string) => + runMutation(() => actions.updateBaseline(resultId)), + isPending: false, + }; +} + +/** One attachment slot for a run result (run-details expansion). */ +export function useResultAttachment(): AttachmentFetcher< + [resultId: string, field: ResultAttachmentField] +> { + const actions = useSolutionTestsActions(); + return { + fetch: (resultId: string, field: ResultAttachmentField) => + actions.getResultAttachment(resultId, field), + }; +} diff --git a/apps/apollo-vertex/registry/solution-tests/use-solution-test-batch-runs.ts b/apps/apollo-vertex/registry/solution-tests/use-solution-test-batch-runs.ts new file mode 100644 index 000000000..5e9d9860e --- /dev/null +++ b/apps/apollo-vertex/registry/solution-tests/use-solution-test-batch-runs.ts @@ -0,0 +1,27 @@ +"use client"; + +/** + * Solution test batch runs: the live batch-run list. The force-stop write for + * a batch lives in `use-force-stop` alongside the single-run stop. + */ + +import { useLiveQuery } from "@tanstack/react-db"; +import { useSolution } from "@uipath/vs-core"; +import { ENTITY, solutionTestCollection } from "./collections"; +import type { SolutionTestBatchRun } from "./types"; + +export interface UseSolutionTestBatchRunsResult { + batchRuns: SolutionTestBatchRun[]; + isLoading: boolean; +} + +/** Live list of batch runs. */ +export function useSolutionTestBatchRuns(): UseSolutionTestBatchRunsResult { + const solution = useSolution(); + const collection = solutionTestCollection( + solution, + ENTITY.batchRuns, + ); + const { data, isLoading } = useLiveQuery(() => collection); + return { batchRuns: data ?? [], isLoading }; +} diff --git a/apps/apollo-vertex/registry/solution-tests/use-solution-test-runs.ts b/apps/apollo-vertex/registry/solution-tests/use-solution-test-runs.ts new file mode 100644 index 000000000..867b8a444 --- /dev/null +++ b/apps/apollo-vertex/registry/solution-tests/use-solution-test-runs.ts @@ -0,0 +1,27 @@ +"use client"; + +/** + * Solution test runs: the live list of all runs (the view filters by batch in + * memory). The force-stop write for a single run lives in `use-force-stop`. + */ + +import { useLiveQuery } from "@tanstack/react-db"; +import { useSolution } from "@uipath/vs-core"; +import { ENTITY, solutionTestCollection } from "./collections"; +import type { SolutionTestRun } from "./types"; + +export interface UseSolutionTestRunsResult { + runs: SolutionTestRun[]; + isLoading: boolean; +} + +/** Live list of all runs (the view filters by batch in memory). */ +export function useSolutionTestRuns(): UseSolutionTestRunsResult { + const solution = useSolution(); + const collection = solutionTestCollection( + solution, + ENTITY.runs, + ); + const { data, isLoading } = useLiveQuery(() => collection); + return { runs: data ?? [], isLoading }; +} diff --git a/apps/apollo-vertex/registry/solution-tests/use-solution-tests.ts b/apps/apollo-vertex/registry/solution-tests/use-solution-tests.ts new file mode 100644 index 000000000..d70704870 --- /dev/null +++ b/apps/apollo-vertex/registry/solution-tests/use-solution-tests.ts @@ -0,0 +1,69 @@ +"use client"; + +/** + * Solution tests: the live test list plus the writes that act on tests — + * run, toggle-active, and delete. Reads stream from the `UiPathSTTests` + * collection; the writes are placeholders pending the vs-core write surface + * (see `mutations`). + */ + +import { useLiveQuery } from "@tanstack/react-db"; +import { useSolution } from "@uipath/vs-core"; +import { ENTITY, solutionTestCollection } from "./collections"; +import { useSolutionTestsActions } from "./context"; +import { type MutationHookResult, runMutation } from "./mutations"; +import type { MutationResult, SolutionTest } from "./types"; + +export interface UseSolutionTestsResult { + tests: SolutionTest[]; + isLoading: boolean; +} + +/** Live list of solution tests. */ +export function useSolutionTests(): UseSolutionTestsResult { + const solution = useSolution(); + const collection = solutionTestCollection( + solution, + ENTITY.tests, + ); + const { data, isLoading } = useLiveQuery(() => collection); + return { tests: data ?? [], isLoading }; +} + +/** Run the given tests (all active tests when omitted). */ +export function useRunTests(): MutationHookResult< + [testIds?: string[]], + MutationResult +> { + const actions = useSolutionTestsActions(); + return { + mutate: (testIds?: string[]) => + runMutation(() => actions.runTests(testIds)), + isPending: false, + }; +} + +/** Toggle a test's active flag. */ +export function useToggleTestActive(): MutationHookResult< + [testId: string, isActive: boolean], + void +> { + const actions = useSolutionTestsActions(); + return { + mutate: (testId: string, isActive: boolean) => + actions.toggleTestActive(testId, isActive), + isPending: false, + }; +} + +/** Delete a test. */ +export function useDeleteTest(): MutationHookResult< + [testId: string], + MutationResult +> { + const actions = useSolutionTestsActions(); + return { + mutate: (testId: string) => runMutation(() => actions.deleteTest(testId)), + isPending: false, + }; +} diff --git a/apps/apollo-vertex/registry/solution-tests/user-messages.ts b/apps/apollo-vertex/registry/solution-tests/user-messages.ts new file mode 100644 index 000000000..7cb69300b --- /dev/null +++ b/apps/apollo-vertex/registry/solution-tests/user-messages.ts @@ -0,0 +1,78 @@ +import { AlertCircle, AlertTriangle, Info } from "lucide-react"; +import type { UserMessageItem } from "./types"; + +const MESSAGE_CATEGORIES = new Set(["Error", "Warning", "Info"]); + +/** Validate an arbitrary parsed value against the UserMessageItem shape. */ +function isUserMessageItem(value: unknown): value is UserMessageItem { + if (typeof value !== "object" || value === null) return false; + return ( + "key" in value && + typeof value.key === "string" && + "category" in value && + typeof value.category === "string" && + MESSAGE_CATEGORIES.has(value.category) && + "message" in value && + typeof value.message === "string" && + "timestamp" in value && + typeof value.timestamp === "string" && + "details" in value && + typeof value.details === "object" && + value.details !== null + ); +} + +export function parseUserMessages(messages: unknown): UserMessageItem[] { + let value: unknown = messages; + if (typeof value === "string") { + try { + value = JSON.parse(value); + } catch { + return []; + } + } + if (!Array.isArray(value)) return []; + const items: UserMessageItem[] = []; + for (const entry of value) { + if (isUserMessageItem(entry)) items.push(entry); + } + return items; +} + +const SEVERITY_ORDER: Record = { + Error: 2, + Warning: 1, + Info: 0, +}; + +export function worstSeverity( + messages: UserMessageItem[], +): "Error" | "Warning" | "Info" | null { + if (messages.length === 0) return null; + let worst: UserMessageItem["category"] = messages[0].category; + for (const msg of messages) { + if ((SEVERITY_ORDER[msg.category] ?? 0) > (SEVERITY_ORDER[worst] ?? 0)) { + worst = msg.category; + } + } + return worst; +} + +export const severityIcons = { + Error: AlertCircle, + Warning: AlertTriangle, + Info: Info, +} as const; + +export const severityColors = { + Error: "text-destructive", + Warning: "text-amber-500", + Info: "text-blue-500", +} as const; + +export const categoryBorderStyles = { + Error: + "border-l-destructive bg-destructive/10 dark:bg-destructive/25 text-foreground", + Warning: "border-l-warning bg-warning/15 dark:bg-warning/25 text-foreground", + Info: "border-l-info bg-info/15 dark:bg-info/25 text-foreground", +} as const; diff --git a/apps/apollo-vertex/registry/solution-tests/utils.ts b/apps/apollo-vertex/registry/solution-tests/utils.ts new file mode 100644 index 000000000..7f9039246 --- /dev/null +++ b/apps/apollo-vertex/registry/solution-tests/utils.ts @@ -0,0 +1,18 @@ +import { RunStatus } from "./types"; + +export function isRunDone(status: number): boolean { + return status !== RunStatus.Pending && status !== RunStatus.Running; +} + +export function hasAutoPass(evaluatorResults: unknown): boolean { + let parsed = evaluatorResults; + if (typeof parsed === "string") { + try { + parsed = JSON.parse(parsed); + } catch { + return false; + } + } + if (typeof parsed !== "object" || parsed === null) return false; + return "auto_pass" in parsed; +} diff --git a/apps/apollo-vertex/types/optional-deps.d.ts b/apps/apollo-vertex/types/optional-deps.d.ts index 40e03662e..f2c564bf7 100644 --- a/apps/apollo-vertex/types/optional-deps.d.ts +++ b/apps/apollo-vertex/types/optional-deps.d.ts @@ -1,13 +1,27 @@ declare module "@tanstack/react-db" { + // Minimal stand-ins for @tanstack/db (re-exported by react-db). The phantom + // `__row` on Collection carries the row type so `useLiveQuery` can infer it; + // QueryResult is opaque (query-builder hooks supply their row type explicitly). + export interface Collection { + readonly __row?: T; + } + interface QueryResult { + readonly __query?: true; + } + interface QueryBuilder { + from(source: Record): QueryResult; + } + + // Accepts both the direct-collection form (`() => collection`, used by the + // Solution Tests read hooks — row type inferred from the Collection) and the + // query-builder form (`(q) => q.from({...})`, used by the identity/entity + // hooks — row type supplied explicitly). export function useLiveQuery( - queryFn: (q: { - from: (source: Record) => unknown; - }) => unknown, + queryFn: ( + q: QueryBuilder, + ) => Collection | QueryResult | undefined | null, deps?: Array, - ): { - data: T[] | undefined; - isLoading: boolean; - }; + ): { data: T[] | undefined; isLoading: boolean }; } declare module "@uipath/proteus-client" { @@ -59,6 +73,10 @@ declare module "@uipath/vs-core" { api: { collections: { entities: Record; + // Dedicated, name-keyed namespace for the Solution Test entity + // collections (UiPathSTTests, UiPathSTBatchRuns, …) — added in + // @uipath/vs-core 2.x. Partial: a given collection may be absent. + solutionTests: Record; identity: { groups: unknown; groupMembers: unknown;