feat(027): Final Phase T038-T043 implementation

- T038: SessionEvent logger and persistence logic
  - Added SessionEventLogger service with explicit audit event persistence
  - Added SessionEvent model with events relationship on DatasetReviewSession
  - Integrated event logging into orchestrator flows and API mutation endpoints

- T039: Semantic source version propagation
  - Added source_version column to SemanticFieldEntry
  - Added propagate_source_version_update() to SemanticResolver
  - Preserves locked/manual field invariants during propagation

- T040: Batch approval API and UI actions
  - Added batch semantic approval endpoint (/fields/semantic/approve-batch)
  - Added batch mapping approval endpoint (/mappings/approve-batch)
  - Added batch approval actions to SemanticLayerReview and ExecutionMappingReview components
  - Aligned batch semantics with single-item approval contracts

- T041: Superset compatibility matrix tests
  - Added test_superset_matrix.py with preview and SQL Lab fallback coverage
  - Tests verify client method preference and matrix fallback behavior

- T042: RBAC audit sweep on session-mutation endpoints
  - Added _require_owner_mutation_scope() helper
  - Applied owner guards to update_session, delete_session, and all mutation endpoints
  - Ensured no bypass of existing permission checks

- T043: i18n coverage for dataset-review UI
  - Added workspace state labels (empty/importing/review) to en.json and ru.json
  - Added batch action labels for semantics and mappings
  - Fixed workspace state comparison to lowercase strings
  - Removed hardcoded workspace state display strings

Signed-off-by: Implementation Specialist <impl@ss-tools>
This commit is contained in:
2026-03-17 14:29:33 +03:00
parent 38bda6a714
commit ed3d5f3039
33 changed files with 99234 additions and 93415 deletions

View File

@@ -14,8 +14,8 @@
<!-- @UX_STATE: Empty -> Show source intake with Superset link and dataset-selection entry actions. -->
<!-- @UX_STATE: Importing -> Show progressive recovery milestones as context is assembled. -->
<!-- @UX_STATE: Review -> Show summary, findings, semantic layer, filters, mapping, and next action. -->
<!-- @UX_STATE: Clarification -> Reserved for later batch; current session still exposes the next-step intent. -->
<!-- @UX_STATE: Ready -> Reserved for later batch; warnings stay visible and never disappear silently. -->
<!-- @UX_STATE: Clarification -> Clarification panel mounts one active question at a time and exposes resumable answer plus feedback actions. -->
<!-- @UX_STATE: Ready -> Preview, warnings, and next-step evidence stay visible without hiding US2 review controls. -->
<!-- @UX_FEEDBACK: Main CTA changes by readiness state and reflects current highest-value next action. -->
<!-- @UX_RECOVERY: Users can save, resume, or reopen an unfinished session without losing context. -->
<!-- @UX_REACTIVITY: Uses Svelte runes for session, readiness, preview, and task state derivation. -->
@@ -26,6 +26,11 @@
import { api } from "$lib/api.js";
import SourceIntakePanel from "$lib/components/dataset-review/SourceIntakePanel.svelte";
import ValidationFindingsPanel from "$lib/components/dataset-review/ValidationFindingsPanel.svelte";
import SemanticLayerReview from "$lib/components/dataset-review/SemanticLayerReview.svelte";
import ClarificationDialog from "$lib/components/dataset-review/ClarificationDialog.svelte";
import ExecutionMappingReview from "$lib/components/dataset-review/ExecutionMappingReview.svelte";
import CompiledSQLPreview from "$lib/components/dataset-review/CompiledSQLPreview.svelte";
import LaunchConfirmationPanel from "$lib/components/dataset-review/LaunchConfirmationPanel.svelte";
import {
setError,
setLoading,
@@ -52,6 +57,9 @@
let summaryDraft = $state("");
let jumpTarget = $state("");
let intakeAcknowledgment = $state("");
let clarificationState = $state(null);
let previewUiState = $state("");
let launchResult = $state(null);
const routeSessionId = $derived(page.params.id || "");
const environments = $derived(environmentContextState.current?.environments || []);
@@ -62,10 +70,10 @@
);
const currentWorkspaceState = $derived(
!session
? (isSubmitting ? "Importing" : "Empty")
? (isSubmitting ? "importing" : "empty")
: session.readiness_state === "importing"
? "Importing"
: "Review",
? "importing"
: "review",
);
const profile = $derived(session?.profile || null);
const findings = $derived(session?.findings || []);
@@ -73,8 +81,14 @@
const latestPreview = $derived(
session?.previews?.length ? session.previews[session.previews.length - 1] : null,
);
const executionMappings = $derived(session?.execution_mappings || []);
const templateVariables = $derived(session?.template_variables || []);
const latestRunContext = $derived(
session?.run_contexts?.length ? session.run_contexts[session.run_contexts.length - 1] : null,
);
const exportDisabled = $derived(!session || isExporting);
const saveDisabled = $derived(!session || isSubmitting);
const launchDisabled = $derived(!session || isSubmitting || isExporting);
const blockingCount = $derived(
findings.filter((finding) => finding.severity === "blocking").length,
);
@@ -104,6 +118,14 @@
const importMilestones = $derived(buildImportMilestones());
const recentActions = $derived(buildRecentActions());
function getWorkspaceStateLabel(state) {
return $t.dataset_review?.workspace?.state?.[state] || state;
}
function getFallbackLabel(key, fallback = "") {
return $t.dataset_review?.workspace?.fallbacks?.[key] || fallback;
}
function getRecommendedActionLabel(action) {
const normalized = String(action || "import_from_superset");
return $t.dataset_review?.workspace?.actions?.[normalized] || normalized;
@@ -114,7 +136,7 @@
const filtersDone = importedFilters.length > 0;
const variablesDone = Boolean(importedFilters.length || latestPreview);
const semanticsDone = Boolean(profile?.business_summary || findings.length > 0);
const importState = currentWorkspaceState === "Importing";
const importState = currentWorkspaceState === "importing";
return [
{
@@ -154,7 +176,10 @@
{
key: "source",
title: $t.dataset_review?.workspace?.timeline?.source,
detail: session.source_input || session.dataset_ref || "—",
detail:
session.source_input ||
session.dataset_ref ||
getFallbackLabel("empty_value", "—"),
},
{
key: "status",
@@ -197,6 +222,7 @@
async function loadSessionDetail(sessionId) {
if (!sessionId) {
session = null;
clarificationState = null;
loadError = "";
summaryDraft = "";
resetSession();
@@ -210,8 +236,11 @@
try {
const detail = await api.fetchApi(`/dataset-orchestration/sessions/${sessionId}`);
session = detail;
previewUiState = "";
launchResult = null;
summaryDraft = detail?.profile?.business_summary || "";
setSession(detail);
await loadClarificationState(detail);
} catch (error) {
loadError =
error?.message ||
@@ -223,6 +252,21 @@
}
}
async function loadClarificationState(sessionDetail) {
if (!sessionDetail?.session_id || !sessionDetail?.clarification_sessions?.length) {
clarificationState = null;
return;
}
try {
clarificationState = await api.fetchApi(
`/dataset-orchestration/sessions/${sessionDetail.session_id}/clarification`,
);
} catch (error) {
clarificationState = null;
}
}
async function bootstrapWorkspace() {
isBootstrapping = true;
await initializeEnvironmentContext();
@@ -341,6 +385,121 @@
isEditingSummary = false;
}
function handleSemanticUpdated(updatedField) {
if (!session || !updatedField) {
return;
}
if (Array.isArray(updatedField.fields)) {
const updatedLookup = new Map(updatedField.fields.map((field) => [field.field_id, field]));
session = {
...session,
semantic_fields: (session.semantic_fields || []).map((field) =>
updatedLookup.get(field.field_id) || field,
),
};
return;
}
if (!updatedField?.field_id) {
return;
}
session = {
...session,
semantic_fields: (session.semantic_fields || []).map((field) =>
field.field_id === updatedField.field_id ? updatedField : field,
),
};
}
function handleClarificationUpdated(result) {
if (!result) {
return;
}
if (result.clarification_state) {
clarificationState = result.clarification_state;
} else if (result.clarification_session || result.current_question !== undefined) {
clarificationState = result;
}
if (result.session && session) {
session = {
...session,
...result.session,
};
}
}
function handleExecutionMappingUpdated(result) {
if (!result || !session) {
return;
}
if (Array.isArray(result.mappings)) {
const updatedLookup = new Map(result.mappings.map((mapping) => [mapping.mapping_id, mapping]));
session = {
...session,
execution_mappings: executionMappings.map((mapping) =>
updatedLookup.get(mapping.mapping_id) || mapping,
),
};
} else if (result.mapping) {
session = {
...session,
execution_mappings: executionMappings.map((mapping) =>
mapping.mapping_id === result.mapping.mapping_id ? result.mapping : mapping,
),
};
} else {
return;
}
if (result.preview_state) {
previewUiState = result.preview_state;
}
}
function handlePreviewUpdated(result) {
if (!session || !result?.preview) {
return;
}
previewUiState = result.preview_state || result.preview.preview_status || "";
const existingPreviews = session.previews || [];
const nextPreviews = existingPreviews.some(
(item) => item.preview_id === result.preview.preview_id,
)
? existingPreviews.map((item) =>
item.preview_id === result.preview.preview_id ? result.preview : item,
)
: [...existingPreviews, result.preview];
session = {
...session,
previews: nextPreviews,
readiness_state:
result.preview.preview_status === "ready"
? session.readiness_state === "run_ready"
? "run_ready"
: "compiled_preview_ready"
: session.readiness_state,
};
}
function handleLaunchUpdated(result) {
if (!result) {
return;
}
launchResult = result.launch_result || result;
}
function handleSectionJump(event) {
jumpTarget = event?.target || "";
}
function primaryActionHandler() {
if (!session) {
return;
@@ -351,6 +510,21 @@
return;
}
if (session.recommended_action === "generate_sql_preview") {
jumpTarget = "sql-preview";
return;
}
if (session.recommended_action === "approve_mapping") {
jumpTarget = "mapping";
return;
}
if (session.recommended_action === "launch_dataset") {
jumpTarget = "launch";
return;
}
if (session.status === "active") {
void updateSessionLifecycle("paused");
}
@@ -383,7 +557,7 @@
</span>
{/if}
<span class="rounded-full bg-slate-100 px-3 py-1 text-xs font-medium text-slate-700">
{$t.dataset_review?.workspace?.state_label}: {currentWorkspaceState}
{$t.dataset_review?.workspace?.state_label}: {getWorkspaceStateLabel(currentWorkspaceState)}
</span>
{#if session}
<span class="rounded-full bg-blue-50 px-3 py-1 text-xs font-medium text-blue-700">
@@ -429,7 +603,7 @@
onsubmit={handleSourceSubmit}
/>
{#if currentWorkspaceState === "Importing"}
{#if currentWorkspaceState === "importing"}
<section class="rounded-2xl border border-blue-200 bg-blue-50 p-5 shadow-sm">
<div class="flex items-start justify-between gap-3">
<div>
@@ -444,7 +618,7 @@
</p>
</div>
<span class="rounded-full bg-white px-3 py-1 text-xs font-medium text-blue-700">
{currentWorkspaceState}
{getWorkspaceStateLabel(currentWorkspaceState)}
</span>
</div>
@@ -499,7 +673,7 @@
{$t.dataset_review?.workspace?.import_status_title}
</div>
<div class="mt-1 text-sm font-medium text-slate-900">
{currentWorkspaceState}
{getWorkspaceStateLabel(currentWorkspaceState)}
</div>
</div>
<span class="rounded-full bg-white px-2.5 py-1 text-xs font-medium text-slate-700">
@@ -571,7 +745,8 @@
{profile?.dataset_name || session.dataset_ref}
</h2>
<p class="mt-1 text-sm text-slate-600">
{profile?.schema_name || "—"}{profile?.database_name || "—"}
{profile?.schema_name || getFallbackLabel("empty_value", "—")}{profile?.database_name ||
getFallbackLabel("empty_value", "—")}
</p>
</div>
@@ -741,6 +916,32 @@
{/if}
</div>
<SemanticLayerReview
sessionId={session.session_id}
semanticSources={session.semantic_sources || []}
fields={session.semantic_fields || []}
disabled={isSubmitting}
onupdated={handleSemanticUpdated}
/>
{#if clarificationState || session.clarification_sessions?.length}
<ClarificationDialog
sessionId={session.session_id}
clarificationState={clarificationState}
disabled={isSubmitting}
onupdated={handleClarificationUpdated}
/>
{/if}
<ExecutionMappingReview
sessionId={session.session_id}
mappings={executionMappings}
importedFilters={importedFilters}
templateVariables={templateVariables}
disabled={launchDisabled}
onupdated={handleExecutionMappingUpdated}
/>
<ValidationFindingsPanel
findings={findings}
recommendedAction={session.recommended_action}
@@ -866,27 +1067,27 @@
{/if}
</section>
<section class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
<h2 class="text-lg font-semibold text-slate-900">
{$t.dataset_review?.workspace?.preview_title}
</h2>
{#if latestPreview}
<div class="mt-3 space-y-2 text-sm text-slate-700">
<p>
{$t.dataset_review?.workspace?.preview_status_label}:
{latestPreview.preview_status}
</p>
<p>
{$t.dataset_review?.workspace?.preview_compiler_label}:
{latestPreview.compiled_by}
</p>
</div>
{:else}
<p class="mt-3 text-sm text-slate-600">
{$t.dataset_review?.workspace?.preview_pending_note}
</p>
{/if}
</section>
<CompiledSQLPreview
sessionId={session.session_id}
preview={latestPreview}
previewState={previewUiState}
disabled={launchDisabled}
onupdated={handlePreviewUpdated}
onjump={handleSectionJump}
/>
<LaunchConfirmationPanel
sessionId={session.session_id}
session={session}
findings={findings}
mappings={executionMappings}
preview={latestPreview}
previewState={previewUiState}
latestRunContext={launchResult?.run_context || latestRunContext}
disabled={launchDisabled}
onupdated={handleLaunchUpdated}
onjump={handleSectionJump}
/>
</aside>
</div>
{/if}

View File

@@ -5,15 +5,16 @@
// [DEF:DatasetReviewWorkspaceUxTests:Module]
// @COMPLEXITY: 3
// @SEMANTICS: dataset-review, workspace, route, ux-tests, review-state, exports, recovery
// @PURPOSE: Verify US1 dataset review workspace flow evidence for empty, load, import, recovery, and export behaviors.
// @PURPOSE: Verify US1-safe and US2-integrated dataset review workspace flow evidence for empty, load, import, semantics, clarification, and export behaviors.
// @LAYER: UI
// @RELATION: [VERIFIES] ->[DatasetReviewWorkspace]
// @UX_STATE: Empty -> Intake-first workspace renders clear starting actions.
// @UX_STATE: Importing -> Progressive milestones remain visible while review context is assembled.
// @UX_STATE: Review -> Workspace exposes summary controls, timeline, findings, and export affordances.
// @UX_STATE: Review -> Workspace exposes summary controls, mounted semantic review, clarification dialog, findings, and export affordances.
// @TEST_CONTRACT: DatasetReviewWorkspaceDependencies -> ObservableWorkspaceUX
// @TEST_SCENARIO: empty_state_renders_source_intake -> No session route data shows intake-first workspace.
// @TEST_SCENARIO: session_load_surfaces_partial_recovery_state -> Existing session route shows review summary, findings, and partial recovery evidence.
// @TEST_SCENARIO: session_load_mounts_us2_semantic_and_clarification_panels -> Existing session route mounts semantic review and clarification UI from real route integration.
// @TEST_SCENARIO: import_submission_transitions_to_review_state -> Intake submit creates a session, navigates, and renders review state.
// @TEST_SCENARIO: export_feedback_surfaces_success_and_error -> Export action exposes both artifact success detail and failure feedback.
// @TEST_EDGE: missing_field -> Empty session context still renders stable workspace copy.
@@ -44,6 +45,7 @@ function createSessionDetail(overrides = {}) {
active_task_id: "task-77",
findings: [
{
finding_id: "finding-1",
title: "Missing semantic description",
code: "SEM-1",
area: "dataset_profile",
@@ -80,6 +82,82 @@ function createSessionDetail(overrides = {}) {
confidence_state: "mostly_confirmed",
business_summary: "Imported business description",
},
semantic_sources: [
{
source_id: "source-1",
display_name: "Trusted dictionary",
source_version: "v1",
},
],
semantic_fields: [
{
field_id: "field-1",
field_name: "customer_name",
field_kind: "dimension",
verbose_name: "Customer name",
description: "Resolved from trusted dictionary",
display_format: "text",
provenance: "dictionary_exact",
source_id: "source-1",
confidence_rank: 1,
is_locked: false,
has_conflict: true,
needs_review: true,
last_changed_by: "system",
candidates: [
{
candidate_id: "candidate-1",
match_type: "exact",
confidence_score: 0.98,
proposed_verbose_name: "Customer name",
proposed_description: "Customer display name",
proposed_display_format: "text",
status: "pending",
},
],
},
],
clarification_sessions: [
{
clarification_session_id: "clarification-1",
status: "active",
current_question_id: "question-1",
resolved_count: 1,
remaining_count: 2,
summary_delta: "Awaiting user confirmation",
},
],
...overrides,
};
}
function createClarificationState(overrides = {}) {
return {
clarification_session: {
clarification_session_id: "clarification-1",
session_id: "session-1",
status: "active",
current_question_id: "question-1",
resolved_count: 1,
remaining_count: 2,
summary_delta: "Awaiting user confirmation",
},
current_question: {
question_id: "question-1",
clarification_session_id: "clarification-1",
topic_ref: "profile.summary",
question_text: "Which customer label should be used?",
why_it_matters: "This label is shown to reviewers.",
current_guess: "Customer name",
options: [
{
option_id: "option-1",
value: "Customer name",
label: "Customer name",
is_recommended: true,
},
],
},
...overrides,
};
}
@@ -239,6 +317,8 @@ vi.mock("$lib/i18n", () => ({
resume_action: "Resume session",
pause_action: "Pause session",
jump_target_label: "Jump target",
semantic_review_visible: "Semantic review visible",
clarification_visible: "Clarification visible",
health_title: "Review health",
open_findings_label: "Open findings",
exports_title: "Exports",
@@ -252,6 +332,108 @@ vi.mock("$lib/i18n", () => ({
preview_status_label: "Preview status",
preview_compiler_label: "Compiled by",
},
semantics: {
eyebrow: "Semantic layer",
title: "Review semantic field values",
description: "Review semantic candidates",
field_count_label: "Fields in review",
empty: "No semantic fields",
unknown_source: "Unknown source",
active_value_label: "Active semantic value",
provenance_label: "Provenance",
confidence_label: "Confidence",
confidence_unset: "No confidence rank",
confidence_rank_label: "Rank",
source_label: "Source",
changed_by_label: "Changed by",
locked_badge: "Locked",
unlocked_badge: "Unlocked",
conflict_badge: "Conflict",
needs_review_badge: "Needs review",
manual_verbose_name_label: "Manual verbose name",
manual_description_label: "Manual description",
manual_display_format_label: "Manual display format",
manual_override_action: "Manual override",
save_manual_action: "Save manual value",
lock_action: "Lock field",
unlock_action: "Unlock field",
feedback_up_action: "Thumbs up",
feedback_down_action: "Thumbs down",
candidates_title: "Candidate options",
candidates_empty: "No candidates",
candidate_description_empty: "No candidate description",
display_format_label: "Display format",
score_label: "Score",
apply_candidate_action: "Apply",
apply_and_lock_action: "Apply and lock",
empty_value: "No value",
provenance: {
unresolved: "Unresolved",
dictionary_exact: "Dictionary exact match",
manual_override: "Manual override",
},
candidate_status: {
pending: "Pending",
accepted: "Accepted",
superseded: "Superseded",
},
messages: {
saving: "Saving semantic decision...",
save_failed: "Failed to save semantic decision.",
manual_override_required: "Manual value required",
manual_saved: "Manual semantic value saved and locked.",
candidate_applied: "Candidate value applied.",
candidate_locked: "Candidate value applied and locked.",
locked: "Field locked against silent overwrite.",
unlocked: "Field unlocked for renewed review.",
feedback_up: "Positive feedback recorded.",
feedback_down: "Negative feedback recorded.",
},
},
clarification: {
eyebrow: "Clarification",
title: "Resolve one ambiguity at a time",
description: "One question at a time",
progress_label: "Clarification progress",
state_label: "Dialog state",
status_label: "Status",
remaining_label: "Remaining questions",
summary_delta_label: "Summary delta",
active_question_label: "Active question",
why_it_matters_label: "Why it matters",
current_guess_label: "Current guess",
current_guess_empty: "No current guess",
topic_label: "Topic",
options_title: "Available options",
recommended_badge: "Recommended",
custom_answer_label: "Custom answer",
answer_action: "Answer with selected option",
custom_answer_action: "Submit custom answer",
skip_action: "Skip for now",
assistant_action: "Open assistant guidance",
expert_review_action: "Send to expert review",
resume_action: "Resume clarification",
completed: "No active clarification question remains.",
feedback_label: "Clarification feedback",
feedback_prompt: "Record whether the clarification result was useful.",
feedback_up_action: "Helpful",
feedback_down_action: "Not helpful",
messages: {
saving: "Saving clarification answer...",
saved: "Clarification answer saved.",
skipped: "Question skipped and left resumable.",
expert_review_requested: "Question escalated to expert review.",
assistant_opened: "Assistant guidance opened.",
resumed: "Clarification resumed.",
resume_failed: "Failed to resume clarification.",
save_failed: "Failed to save clarification answer.",
feedback_up: "Positive clarification feedback recorded.",
feedback_down: "Negative clarification feedback recorded.",
feedback_failed: "Failed to save clarification feedback.",
option_required: "Option required",
custom_required: "Custom answer required",
},
},
},
});
return () => {};
@@ -311,7 +493,9 @@ describe("DatasetReviewWorkspace UX Contract", () => {
it("session_load_surfaces_partial_recovery_state", async () => {
routeState.id = "session-1";
api.fetchApi.mockResolvedValue(createSessionDetail());
api.fetchApi
.mockResolvedValueOnce(createSessionDetail())
.mockResolvedValueOnce(createClarificationState());
render(DatasetReviewWorkspace);
@@ -319,10 +503,13 @@ describe("DatasetReviewWorkspace UX Contract", () => {
expect(api.fetchApi).toHaveBeenCalledWith("/dataset-orchestration/sessions/session-1");
});
await waitFor(() => {
expect(screen.getByText("Business summary")).toBeDefined();
});
expect(screen.getByText("Workspace state: Review")).toBeDefined();
expect(screen.getByText("Readiness: Partially ready")).toBeDefined();
expect(screen.getAllByText("Sales Dataset").length).toBeGreaterThan(0);
expect(screen.getByText("Partial recovery")).toBeDefined();
expect(screen.getAllByText("Recovered filters").length).toBeGreaterThan(0);
expect(screen.getByText("Imported business description")).toBeDefined();
expect(screen.getAllByRole("button", { name: "Save session" }).length).toBeGreaterThan(0);
@@ -332,9 +519,29 @@ describe("DatasetReviewWorkspace UX Contract", () => {
expect(screen.getByText("Source accepted")).toBeDefined();
});
it("session_load_mounts_us2_semantic_and_clarification_panels", async () => {
routeState.id = "session-1";
api.fetchApi
.mockResolvedValueOnce(createSessionDetail())
.mockResolvedValueOnce(createClarificationState());
render(DatasetReviewWorkspace);
await waitFor(() => {
expect(screen.getByText("Review semantic field values")).toBeDefined();
});
expect(screen.getByText("Resolve one ambiguity at a time")).toBeDefined();
expect(screen.getByText("customer_name")).toBeDefined();
expect(screen.getByText("Which customer label should be used?")).toBeDefined();
expect(screen.getAllByText("Customer name").length).toBeGreaterThan(0);
});
it("summary_edit_affordance_updates_visible_draft", async () => {
routeState.id = "session-1";
api.fetchApi.mockResolvedValue(createSessionDetail());
api.fetchApi
.mockResolvedValueOnce(createSessionDetail())
.mockResolvedValueOnce(createClarificationState());
render(DatasetReviewWorkspace);
@@ -344,7 +551,7 @@ describe("DatasetReviewWorkspace UX Contract", () => {
await fireEvent.click(screen.getByRole("button", { name: "[Edit]" }));
const summaryEditor = screen.getByRole("textbox");
const summaryEditor = screen.getAllByRole("textbox")[0];
await fireEvent.input(summaryEditor, {
target: { value: "Updated analyst-facing summary" },
});
@@ -361,12 +568,17 @@ describe("DatasetReviewWorkspace UX Contract", () => {
const createdSession = createSessionDetail();
let resolveSessionLoad;
api.postApi.mockResolvedValue({ session_id: "session-1" });
api.fetchApi.mockImplementation(
() =>
new Promise((resolve) => {
api.fetchApi.mockImplementation((url) => {
if (url === "/dataset-orchestration/sessions/session-1") {
return new Promise((resolve) => {
resolveSessionLoad = resolve;
}),
);
});
}
if (url === "/dataset-orchestration/sessions/session-1/clarification") {
return Promise.resolve(createClarificationState());
}
return Promise.resolve(undefined);
});
render(DatasetReviewWorkspace);
@@ -417,6 +629,7 @@ describe("DatasetReviewWorkspace UX Contract", () => {
routeState.id = "session-1";
api.fetchApi
.mockResolvedValueOnce(createSessionDetail())
.mockResolvedValueOnce(createClarificationState())
.mockResolvedValueOnce({
artifact_type: "documentation",
format: "json",