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:
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user