G0~G8 성과·동맹 측정 OS 작업 일괄 고정
8월 7일까지 워킹트리에만 남아 있던 미커밋 작업을 커밋한다. 여러 사본 폴더(worktree·clone)에 흩어져 있던 중간 스냅샷을 정리하기 전에 원본을 git 이력으로 고정하는 것이 목적이다. - contracts/routes/services: measurement, outcome_trajectory, rupture_repair, deliberate_practice, calibration_transfer, supervision_research, multimodal_alliance, continuous_improvement 계열 신규 모듈과 테스트 - infra/db/init: 07~16 마이그레이션(측정 기반~calibration transfer 실행) - apps/web: 세션 리뷰 카드·관리 화면·E2E 스펙 추가 - docs/ops: G0~G8 라이브 통합·배포·롤백 증거 문서와 evidence JSON/PNG - scripts: smoke·ledger·릴리스 에이전트·NAS 프리뷰 운영 스크립트 engine.public 로그 .bak과 apps/web/test-results 산출물은 커밋에서 제외했다.
This commit is contained in:
parent
93dd8f82d7
commit
16e791e044
390 changed files with 243188 additions and 499 deletions
541
apps/api/app/test_outcome_trajectory_store.py
Normal file
541
apps/api/app/test_outcome_trajectory_store.py
Normal file
|
|
@ -0,0 +1,541 @@
|
|||
"""Focused G2 persistence adapter and HTTP boundary tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import ValidationError
|
||||
|
||||
from .contracts.outcome_trajectory import LongitudinalOutcomeInput
|
||||
from .deps import Principal, Role, get_current_principal
|
||||
from .routes import outcome_trajectories
|
||||
from .services import outcome_trajectory_store
|
||||
from .services.outcome_trajectory import (
|
||||
assess_longitudinal_outcome,
|
||||
load_trajectory_benchmark,
|
||||
)
|
||||
|
||||
|
||||
SESSION_ID = UUID("00000000-0000-0000-0000-00000000b101")
|
||||
MEASUREMENT_ID = UUID("00000000-0000-0000-0000-00000000b102")
|
||||
TURN_ID = UUID("00000000-0000-0000-0000-00000000b103")
|
||||
REVISION_ID = UUID("00000000-0000-0000-0000-00000000b104")
|
||||
SUBMISSION_ID = UUID("00000000-0000-0000-0000-00000000b108")
|
||||
MEMORY_EVENT_ID = UUID("00000000-0000-0000-0000-00000000b109")
|
||||
BENCHMARK_PATH = (
|
||||
Path(__file__).resolve().parent
|
||||
/ "data"
|
||||
/ "outcome_trajectory_benchmark_g2.v1.json"
|
||||
)
|
||||
|
||||
|
||||
def _principal(role: Role = Role.LEARNER) -> Principal:
|
||||
return Principal(
|
||||
user_id="00000000-0000-0000-0000-00000000b105",
|
||||
role=role,
|
||||
cohort_ids=["e2e-hanshin"],
|
||||
)
|
||||
|
||||
|
||||
def _measurement(**overrides: object) -> dict[str, object]:
|
||||
row: dict[str, object] = {
|
||||
"measurement_id": MEASUREMENT_ID,
|
||||
"status": "ready",
|
||||
"error_code": None,
|
||||
"value": 6.0,
|
||||
"scale_min": 0.0,
|
||||
"scale_max": 10.0,
|
||||
"confidence": 0.8,
|
||||
"source_kind": "simulated_state",
|
||||
"perspective": "client_simulation",
|
||||
"instrument_id": "vignette-session-outcome",
|
||||
"instrument_version": "1.0.0",
|
||||
"model_run_id": None,
|
||||
"evidence_turn_ids": (TURN_ID,),
|
||||
"created_at": datetime(2026, 8, 6, 8, 0, tzinfo=UTC),
|
||||
}
|
||||
row.update(overrides)
|
||||
return row
|
||||
|
||||
|
||||
class OutcomeObservationAdapterTest(unittest.TestCase):
|
||||
def test_ready_measurement_is_normalized_with_full_provenance(self) -> None:
|
||||
observation, snapshot = outcome_trajectory_store._observation_from_measurement(
|
||||
session_no=1,
|
||||
axis="distress_load",
|
||||
measurement=_measurement(),
|
||||
)
|
||||
|
||||
self.assertEqual(observation.status, "observed")
|
||||
self.assertEqual(observation.value, 0.6)
|
||||
self.assertEqual(observation.evidence_refs, (str(TURN_ID),))
|
||||
self.assertEqual(snapshot["raw_value"], 6.0)
|
||||
self.assertEqual(snapshot["measurement_id"], MEASUREMENT_ID)
|
||||
|
||||
def test_simulated_state_without_turn_ids_uses_measurement_provenance(self) -> None:
|
||||
observation, snapshot = outcome_trajectory_store._observation_from_measurement(
|
||||
session_no=1,
|
||||
axis="daily_functioning",
|
||||
measurement=_measurement(evidence_turn_ids=()),
|
||||
)
|
||||
|
||||
self.assertEqual(observation.status, "observed")
|
||||
self.assertEqual(observation.evidence_refs, (f"measurement:{MEASUREMENT_ID}",))
|
||||
self.assertEqual(snapshot["raw_value"], 6.0)
|
||||
|
||||
def test_learner_self_report_is_its_own_evidence_without_turn_ids(self) -> None:
|
||||
observation, snapshot = outcome_trajectory_store._observation_from_measurement(
|
||||
session_no=1,
|
||||
axis="daily_functioning",
|
||||
measurement=_measurement(
|
||||
source_kind="learner_reported",
|
||||
perspective="learner_self_report",
|
||||
instrument_id="vignette-session-outcome-checkin",
|
||||
evidence_turn_ids=(),
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(observation.status, "observed")
|
||||
self.assertEqual(
|
||||
observation.evidence_refs,
|
||||
(f"measurement:{MEASUREMENT_ID}",),
|
||||
)
|
||||
self.assertEqual(snapshot["evidence_refs"], observation.evidence_refs)
|
||||
|
||||
def test_model_observation_still_requires_transcript_evidence(self) -> None:
|
||||
observation, _ = outcome_trajectory_store._observation_from_measurement(
|
||||
session_no=1,
|
||||
axis="daily_functioning",
|
||||
measurement=_measurement(
|
||||
source_kind="model_inferred",
|
||||
perspective="independent_observer",
|
||||
model_run_id=UUID("00000000-0000-0000-0000-00000000b110"),
|
||||
evidence_turn_ids=(),
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(observation.status, "missing")
|
||||
self.assertEqual(observation.missing_reason, "measurement_evidence_missing")
|
||||
|
||||
def test_error_measurement_never_reuses_its_stale_value(self) -> None:
|
||||
observation, snapshot = outcome_trajectory_store._observation_from_measurement(
|
||||
session_no=1,
|
||||
axis="learning_engagement",
|
||||
measurement=_measurement(status="error", error_code="engine_timeout"),
|
||||
)
|
||||
|
||||
self.assertEqual(observation.status, "error")
|
||||
self.assertIsNone(observation.value)
|
||||
self.assertEqual(
|
||||
observation.missing_reason, "measurement_error:engine_timeout"
|
||||
)
|
||||
self.assertIsNone(snapshot["value"])
|
||||
|
||||
def test_fingerprint_changes_with_source_measurement_revision(self) -> None:
|
||||
_, snapshot = outcome_trajectory_store._observation_from_measurement(
|
||||
session_no=1,
|
||||
axis="distress_load",
|
||||
measurement=_measurement(),
|
||||
)
|
||||
changed = dict(snapshot)
|
||||
changed["measurement_id"] = UUID(
|
||||
"00000000-0000-0000-0000-00000000b106"
|
||||
)
|
||||
|
||||
first = outcome_trajectory_store._evidence_fingerprint(
|
||||
expected_arc_hash="a" * 64, snapshots=[snapshot]
|
||||
)
|
||||
second = outcome_trajectory_store._evidence_fingerprint(
|
||||
expected_arc_hash="a" * 64, snapshots=[changed]
|
||||
)
|
||||
|
||||
self.assertNotEqual(first, second)
|
||||
|
||||
def test_human_view_keeps_learner_and_supervisor_memory_separate(self) -> None:
|
||||
self.assertEqual(outcome_trajectory_store._human_view(_principal()), "counselor")
|
||||
self.assertEqual(
|
||||
outcome_trajectory_store._human_view(_principal(Role.TEACHER)),
|
||||
"supervisor",
|
||||
)
|
||||
|
||||
def test_submission_idempotency_returns_same_three_measurements(self) -> None:
|
||||
submission_hash = outcome_trajectory_store._submission_hash(
|
||||
submission_id=SUBMISSION_ID,
|
||||
scores={axis: 0.5 for axis in ("distress_load", "daily_functioning", "learning_engagement")},
|
||||
confidences={axis: 0.8 for axis in ("distress_load", "daily_functioning", "learning_engagement")},
|
||||
evidence_turn_ids=(),
|
||||
)
|
||||
ids = [
|
||||
UUID("00000000-0000-0000-0000-00000000b11" + str(index))
|
||||
for index in range(1, 4)
|
||||
]
|
||||
rows = [
|
||||
{
|
||||
"measurement_id": measurement_id,
|
||||
"dimension": axis,
|
||||
"metadata": {"submission_hash": submission_hash},
|
||||
}
|
||||
for axis, measurement_id in zip(
|
||||
("distress_load", "daily_functioning", "learning_engagement"),
|
||||
ids,
|
||||
strict=True,
|
||||
)
|
||||
]
|
||||
|
||||
self.assertEqual(
|
||||
outcome_trajectory_store._existing_submission_measurement_ids(
|
||||
rows, submission_hash=submission_hash
|
||||
),
|
||||
ids,
|
||||
)
|
||||
with self.assertRaises(outcome_trajectory_store.OutcomeTrajectoryConflictError):
|
||||
outcome_trajectory_store._existing_submission_measurement_ids(
|
||||
rows, submission_hash="c" * 64
|
||||
)
|
||||
|
||||
|
||||
class OutcomeTrajectoryRouteTest(unittest.IsolatedAsyncioTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
pack = load_trajectory_benchmark(BENCHMARK_PATH)
|
||||
assessment = assess_longitudinal_outcome(
|
||||
LongitudinalOutcomeInput(
|
||||
expected_arc=pack.expected_arc,
|
||||
sessions=(pack.cases[0].sessions[0],),
|
||||
)
|
||||
).model_dump(mode="json")
|
||||
assessment["sessions"][0]["safety_signals"] = []
|
||||
cls.payload = {
|
||||
"session_id": SESSION_ID,
|
||||
"revision_id": REVISION_ID,
|
||||
"revision_no": 1,
|
||||
"supersedes_revision_id": None,
|
||||
"source_fingerprint": "b" * 64,
|
||||
"recompute_reason": "initial_computation",
|
||||
"computed_at": "2026-08-06T08:00:00+00:00",
|
||||
"notice_ko": outcome_trajectory_store.NON_CLINICAL_NOTICE_KO,
|
||||
"expected_arc": {
|
||||
"schema_version": pack.expected_arc.schema_version,
|
||||
"arc_id": pack.expected_arc.arc_id,
|
||||
"title_ko": pack.expected_arc.title_ko,
|
||||
"data_classification": "synthetic_educational",
|
||||
"clinical_claim_allowed": False,
|
||||
"provenance_note": pack.expected_arc.provenance_note,
|
||||
"session_count": 5,
|
||||
"distributions": [
|
||||
item.model_dump(mode="json")
|
||||
for item in pack.expected_arc.distributions
|
||||
],
|
||||
},
|
||||
"assessment": assessment,
|
||||
"next_questions": [],
|
||||
"observations": [
|
||||
{
|
||||
"measurement_id": MEASUREMENT_ID,
|
||||
"session_id": SESSION_ID,
|
||||
"session_no": 1,
|
||||
"axis": axis,
|
||||
"status": "observed",
|
||||
"value": 0.5,
|
||||
"raw_value": 0.5,
|
||||
"scale_min": 0.0,
|
||||
"scale_max": 1.0,
|
||||
"confidence": 0.8,
|
||||
"source_kind": "simulated_state",
|
||||
"perspective": "client_simulation",
|
||||
"instrument_id": "vignette-session-outcome",
|
||||
"instrument_version": "1.0.0",
|
||||
"model_run_id": None,
|
||||
"evidence_refs": [str(TURN_ID)],
|
||||
"missing_reason": None,
|
||||
"occurred_at": "2026-08-06T07:59:00+00:00",
|
||||
}
|
||||
for axis in (
|
||||
"distress_load",
|
||||
"daily_functioning",
|
||||
"learning_engagement",
|
||||
)
|
||||
],
|
||||
"safety_signals": [
|
||||
{
|
||||
"safety_event_id": "44",
|
||||
"session_no": 1,
|
||||
"risk_level": "high",
|
||||
"escalated": True,
|
||||
"evidence_refs": [str(TURN_ID)],
|
||||
}
|
||||
],
|
||||
"relationship_memory": [],
|
||||
}
|
||||
|
||||
async def test_get_exposes_synthetic_nonclinical_label_and_separate_safety(self) -> None:
|
||||
with patch.object(
|
||||
outcome_trajectories.outcome_trajectory_store,
|
||||
"read_outcome_trajectory",
|
||||
AsyncMock(return_value=self.payload),
|
||||
) as read:
|
||||
response = await outcome_trajectories.get_outcome_trajectory(
|
||||
SESSION_ID, _principal()
|
||||
)
|
||||
|
||||
self.assertFalse(response.expected_arc.clinical_claim_allowed)
|
||||
self.assertEqual(
|
||||
response.expected_arc.data_classification, "synthetic_educational"
|
||||
)
|
||||
self.assertEqual(len(response.safety_signals), 1)
|
||||
self.assertEqual(response.assessment.sessions[0].safety_signals, ())
|
||||
read.assert_awaited_once_with(
|
||||
principal=unittest.mock.ANY,
|
||||
session_id=SESSION_ID,
|
||||
)
|
||||
|
||||
async def test_recompute_always_requests_new_revision(self) -> None:
|
||||
changed = dict(self.payload)
|
||||
changed.update(
|
||||
{
|
||||
"revision_no": 2,
|
||||
"supersedes_revision_id": REVISION_ID,
|
||||
"revision_id": UUID("00000000-0000-0000-0000-00000000b107"),
|
||||
"recompute_reason": "교수자 재검토",
|
||||
}
|
||||
)
|
||||
with patch.object(
|
||||
outcome_trajectories.outcome_trajectory_store,
|
||||
"read_outcome_trajectory",
|
||||
AsyncMock(return_value=changed),
|
||||
) as read:
|
||||
response = await outcome_trajectories.recompute_outcome_trajectory(
|
||||
SESSION_ID,
|
||||
outcome_trajectories.OutcomeTrajectoryRecomputeRequest(
|
||||
reason=" 교수자 재검토 "
|
||||
),
|
||||
_principal(Role.TEACHER),
|
||||
)
|
||||
|
||||
self.assertEqual(response.revision_no, 2)
|
||||
self.assertEqual(response.supersedes_revision_id, REVISION_ID)
|
||||
read.assert_awaited_once_with(
|
||||
principal=unittest.mock.ANY,
|
||||
session_id=SESSION_ID,
|
||||
force_recompute=True,
|
||||
recompute_reason="교수자 재검토",
|
||||
)
|
||||
|
||||
async def test_hidden_session_maps_to_404(self) -> None:
|
||||
with patch.object(
|
||||
outcome_trajectories.outcome_trajectory_store,
|
||||
"read_outcome_trajectory",
|
||||
AsyncMock(
|
||||
side_effect=outcome_trajectory_store.OutcomeTrajectoryNotFoundError(
|
||||
"session not found or not visible"
|
||||
)
|
||||
),
|
||||
):
|
||||
with self.assertRaises(HTTPException) as raised:
|
||||
await outcome_trajectories.get_outcome_trajectory(
|
||||
SESSION_ID, _principal(Role.TEACHER)
|
||||
)
|
||||
|
||||
self.assertEqual(raised.exception.status_code, 404)
|
||||
|
||||
async def test_outcome_submission_returns_latest_trajectory_and_event_ids(self) -> None:
|
||||
payload = dict(self.payload)
|
||||
payload.update(
|
||||
{
|
||||
"submission_id": SUBMISSION_ID,
|
||||
"submitted_measurement_ids": [
|
||||
UUID("00000000-0000-0000-0000-00000000b111"),
|
||||
UUID("00000000-0000-0000-0000-00000000b112"),
|
||||
UUID("00000000-0000-0000-0000-00000000b113"),
|
||||
],
|
||||
}
|
||||
)
|
||||
body = outcome_trajectories.OutcomeObservationSubmissionRequest(
|
||||
submission_id=SUBMISSION_ID,
|
||||
scores=outcome_trajectories.OutcomeAxisValues(
|
||||
distress_load=0.7,
|
||||
daily_functioning=0.4,
|
||||
learning_engagement=0.8,
|
||||
),
|
||||
confidences=outcome_trajectories.OutcomeAxisValues(
|
||||
distress_load=0.9,
|
||||
daily_functioning=0.8,
|
||||
learning_engagement=0.85,
|
||||
),
|
||||
)
|
||||
with patch.object(
|
||||
outcome_trajectories.outcome_trajectory_store,
|
||||
"submit_outcome_observations",
|
||||
AsyncMock(return_value=payload),
|
||||
) as submit:
|
||||
response = await outcome_trajectories.create_outcome_observations(
|
||||
SESSION_ID, body, _principal()
|
||||
)
|
||||
|
||||
self.assertEqual(response.submission_id, SUBMISSION_ID)
|
||||
self.assertEqual(len(response.submitted_measurement_ids), 3)
|
||||
self.assertEqual(response.revision_id, REVISION_ID)
|
||||
submit.assert_awaited_once_with(
|
||||
principal=unittest.mock.ANY,
|
||||
session_id=SESSION_ID,
|
||||
submission_id=SUBMISSION_ID,
|
||||
scores={
|
||||
"distress_load": 0.7,
|
||||
"daily_functioning": 0.4,
|
||||
"learning_engagement": 0.8,
|
||||
},
|
||||
confidences={
|
||||
"distress_load": 0.9,
|
||||
"daily_functioning": 0.8,
|
||||
"learning_engagement": 0.85,
|
||||
},
|
||||
evidence_turn_ids=(),
|
||||
)
|
||||
|
||||
async def test_submission_content_conflict_maps_to_409(self) -> None:
|
||||
body = outcome_trajectories.OutcomeObservationSubmissionRequest(
|
||||
submission_id=SUBMISSION_ID,
|
||||
scores=outcome_trajectories.OutcomeAxisValues(
|
||||
distress_load=0.5,
|
||||
daily_functioning=0.5,
|
||||
learning_engagement=0.5,
|
||||
),
|
||||
confidences=outcome_trajectories.OutcomeAxisValues(
|
||||
distress_load=0.8,
|
||||
daily_functioning=0.8,
|
||||
learning_engagement=0.8,
|
||||
),
|
||||
)
|
||||
with patch.object(
|
||||
outcome_trajectories.outcome_trajectory_store,
|
||||
"submit_outcome_observations",
|
||||
AsyncMock(
|
||||
side_effect=outcome_trajectory_store.OutcomeTrajectoryConflictError(
|
||||
"submission_id was already used"
|
||||
)
|
||||
),
|
||||
):
|
||||
with self.assertRaises(HTTPException) as raised:
|
||||
await outcome_trajectories.create_outcome_observations(
|
||||
SESSION_ID, body, _principal()
|
||||
)
|
||||
|
||||
self.assertEqual(raised.exception.status_code, 409)
|
||||
|
||||
async def test_relationship_memory_route_preserves_resolve_and_evidence(self) -> None:
|
||||
body = outcome_trajectories.RelationshipMemoryCreateRequest(
|
||||
event_type="repair_confirmed",
|
||||
summaries={
|
||||
"supervisor": "과제 부담을 재확인하고 더 작은 연습으로 합의했다."
|
||||
},
|
||||
evidence_turn_ids=(TURN_ID,),
|
||||
resolves_event_id=MEMORY_EVENT_ID,
|
||||
)
|
||||
created_id = UUID("00000000-0000-0000-0000-00000000b114")
|
||||
with patch.object(
|
||||
outcome_trajectories.outcome_trajectory_store,
|
||||
"append_relationship_memory_event",
|
||||
AsyncMock(return_value=created_id),
|
||||
) as append:
|
||||
response = await outcome_trajectories.create_relationship_memory_event(
|
||||
SESSION_ID, body, _principal(Role.TEACHER)
|
||||
)
|
||||
|
||||
self.assertEqual(response.memory_event_id, created_id)
|
||||
append.assert_awaited_once_with(
|
||||
principal=unittest.mock.ANY,
|
||||
session_id=SESSION_ID,
|
||||
event_type="repair_confirmed",
|
||||
summaries={
|
||||
"supervisor": "과제 부담을 재확인하고 더 작은 연습으로 합의했다."
|
||||
},
|
||||
evidence_turn_ids=(TURN_ID,),
|
||||
resolves_event_id=MEMORY_EVENT_ID,
|
||||
)
|
||||
|
||||
def test_relationship_request_rejects_invalid_resolve_contract(self) -> None:
|
||||
with self.assertRaises(ValidationError):
|
||||
outcome_trajectories.RelationshipMemoryCreateRequest(
|
||||
event_type="goal_agreement",
|
||||
summaries={"supervisor": "목표 합의"},
|
||||
evidence_turn_ids=(TURN_ID,),
|
||||
resolves_event_id=MEMORY_EVENT_ID,
|
||||
)
|
||||
with self.assertRaises(ValidationError):
|
||||
outcome_trajectories.RelationshipMemoryCreateRequest(
|
||||
event_type="repair_confirmed",
|
||||
summaries={"supervisor": "복구 확인"},
|
||||
evidence_turn_ids=(TURN_ID,),
|
||||
)
|
||||
|
||||
async def test_learner_cannot_author_relationship_memory(self) -> None:
|
||||
with self.assertRaises(outcome_trajectory_store.OutcomeTrajectoryStateError):
|
||||
await outcome_trajectory_store.append_relationship_memory_event(
|
||||
principal=_principal(Role.LEARNER),
|
||||
session_id=SESSION_ID,
|
||||
event_type="goal_agreement",
|
||||
summaries={"counselor": "목표 합의"},
|
||||
evidence_turn_ids=(TURN_ID,),
|
||||
)
|
||||
|
||||
|
||||
class OutcomeTrajectoryHttpRoleTest(unittest.TestCase):
|
||||
def _client(self, principal: Principal) -> TestClient:
|
||||
app = FastAPI()
|
||||
app.include_router(outcome_trajectories.router)
|
||||
app.dependency_overrides[get_current_principal] = lambda: principal
|
||||
return TestClient(app)
|
||||
|
||||
def test_learner_is_denied_relationship_memory_authoring(self) -> None:
|
||||
with patch.object(
|
||||
outcome_trajectories.outcome_trajectory_store,
|
||||
"append_relationship_memory_event",
|
||||
AsyncMock(),
|
||||
) as append:
|
||||
response = self._client(_principal(Role.LEARNER)).post(
|
||||
f"/sessions/{SESSION_ID}/relationship-memory-events",
|
||||
json={
|
||||
"event_type": "goal_agreement",
|
||||
"summaries": {"counselor": "회기 목표를 합의했다."},
|
||||
"evidence_turn_ids": [str(TURN_ID)],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
append.assert_not_awaited()
|
||||
|
||||
def test_teacher_is_denied_learner_outcome_submission(self) -> None:
|
||||
with patch.object(
|
||||
outcome_trajectories.outcome_trajectory_store,
|
||||
"submit_outcome_observations",
|
||||
AsyncMock(),
|
||||
) as submit:
|
||||
response = self._client(_principal(Role.TEACHER)).post(
|
||||
f"/sessions/{SESSION_ID}/outcome-observations",
|
||||
json={
|
||||
"submission_id": str(SUBMISSION_ID),
|
||||
"scores": {
|
||||
"distress_load": 0.5,
|
||||
"daily_functioning": 0.5,
|
||||
"learning_engagement": 0.5,
|
||||
},
|
||||
"confidences": {
|
||||
"distress_load": 0.8,
|
||||
"daily_functioning": 0.8,
|
||||
"learning_engagement": 0.8,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
submit.assert_not_awaited()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue