diff --git a/documentation/CheckAction.md b/documentation/CheckAction.md
new file mode 100644
index 000000000..45cbe8491
--- /dev/null
+++ b/documentation/CheckAction.md
@@ -0,0 +1,296 @@
+## CheckAction
+
+This document explains how `org.hyperskill.academy.learning.actions.CheckAction` works, what code paths it executes, and how local and remote checking are combined.
+
+### Entry point
+
+The action is registered in `intellij-plugin/hs-core/resources/META-INF/hs-core.xml`:
+
+```xml
+
+```
+
+The implementation is in `intellij-plugin/hs-core/src/org/hyperskill/academy/learning/actions/CheckAction.kt`.
+
+### High-level flow
+
+`CheckAction.actionPerformed(...)` does the following:
+
+1. Retrieves `Project` from the action event.
+2. Refuses to run while indexing is active (`DumbService.isDumb(project)`).
+3. Clears the check details view.
+4. Saves all open documents.
+5. Reads the current task from `TaskToolWindowView`.
+6. Acquires a per-project lock so only one check can run at a time.
+7. Calls all registered `CheckListener.beforeCheck(...)`.
+8. Starts a background task `StudyCheckTask`.
+
+Relevant code:
+
+- `CheckAction.kt`, `actionPerformed`
+- `CheckAction.kt`, `CheckActionState`
+
+### Core execution graph
+
+```text
+CheckAction.actionPerformed
+ -> CheckListener.beforeCheck(project, task)
+ -> StudyCheckTask.run(indicator)
+ -> localCheck(indicator)
+ -> recreateTestFiles(taskDir)
+ -> maybe createTests(invisibleTestFiles)
+ -> checker.check(indicator)
+ -> if local result is Failed: stop
+ -> remoteCheckerForTask(project, task)
+ -> remoteChecker?.check(project, task, indicator) ?: localResult
+ -> onSuccess()
+ -> update task.status
+ -> update task.feedback
+ -> saveItem(task)
+ -> checker.onTaskSolved() / checker.onTaskFailed()
+ -> TaskToolWindowView.checkFinished(...)
+ -> CheckListener.afterCheck(project, task, result)
+```
+
+### How the local checker is chosen
+
+`StudyCheckTask` creates a local checker through the course configurator:
+
+```kotlin
+val configurator = task.course.configurator
+checker = configurator?.taskCheckerProvider?.getTaskChecker(task, project)
+```
+
+The generic selection logic is in `intellij-plugin/hs-core/src/org/hyperskill/academy/learning/checker/TaskCheckerProvider.kt`.
+
+Important behavior:
+
+- `RemoteEduTask` -> no local checker
+- `CodeTask` -> no local checker
+- `TheoryTask` -> no local checker
+- `UnsupportedTask` -> no local checker
+- `EduTask` -> configurator-specific local checker
+- `OutputTask` -> `OutputTaskChecker`
+- `IdeTask` -> `IdeTaskChecker`
+
+This is why not every task goes through local tests.
+
+### What `localCheck(...)` does
+
+`StudyCheckTask.localCheck(...)` performs the local phase:
+
+1. If no checker exists, returns `CheckResult.NO_LOCAL_CHECK`.
+2. Resolves the task directory.
+3. Recreates test files from task metadata before running checks.
+4. For non-Hyperskill courses, restores invisible test files into the project tree.
+5. Calls `checker.check(indicator)`.
+
+Relevant methods:
+
+- `CheckAction.kt`, `localCheck`
+- `CheckAction.kt`, `recreateTestFiles`
+- `CheckAction.kt`, `createTests`
+
+### Why test files are recreated
+
+Before running a local check, the plugin restores author-provided test files from the task model. This prevents a learner from changing, deleting, or corrupting tests to fake a successful check.
+
+For framework lessons, the plugin uses `FrameworkLessonManager` cached original test files rather than the current `task.taskFiles`, because framework tasks may otherwise carry stale test data from another stage.
+
+### How local test execution works
+
+The standard base implementation for local `EduTask` checking is `EduTaskCheckerBase` in:
+
+`intellij-plugin/hs-core/src/org/hyperskill/academy/learning/checker/EduTaskCheckerBase.kt`
+
+Its `check(...)` method:
+
+1. Hides the Run tool window.
+2. Calls `EnvironmentChecker.getEnvironmentError(project, task)`.
+3. Builds run configurations.
+4. Validates each configuration.
+5. Executes them using IntelliJ run infrastructure.
+6. Collects test results.
+7. Produces a `CheckResult`.
+
+If execution starts but tests do not actually run, subclasses may translate stderr into a more specific error result.
+
+### How run configurations are built and executed
+
+The utility layer is in:
+
+`intellij-plugin/hs-core/src/org/hyperskill/academy/learning/checker/CheckUtils.kt`
+
+Key pieces:
+
+- `getCustomRunConfigurationForChecker(...)`
+- `createDefaultRunConfiguration(...)`
+- `executeRunConfigurations(...)`
+
+Execution details:
+
+- A custom task-specific run configuration in `.idea/runConfigurations` is preferred if present.
+- Otherwise, IntelliJ derives temporary run configurations from PSI context.
+- Configurations run through `ProgramRunner` and `ExecutionEnvironmentBuilder`.
+- The plugin tracks all started environments and waits on a `CountDownLatch`.
+- Test results are collected through a `TestResultCollector`.
+
+### Local-vs-remote ordering
+
+`StudyCheckTask.run(...)` always does the local phase first:
+
+```kotlin
+val localCheckResult = localCheck(indicator)
+if (localCheckResult.status === CheckStatus.Failed) {
+ result = localCheckResult
+ return
+}
+val remoteChecker = remoteCheckerForTask(project, task)
+result = remoteChecker?.check(project, task, indicator) ?: localCheckResult
+```
+
+This means:
+
+- A local `Failed` result stops the pipeline immediately.
+- Remote checking is attempted only if local checking did not fail.
+- If there is no remote checker, the final result is the local result.
+
+### How the remote checker is chosen
+
+Remote checkers are selected through the extension point:
+
+`HyperskillEducational.remoteTaskChecker`
+
+The manager is:
+
+`intellij-plugin/hs-core/src/org/hyperskill/academy/learning/checker/remote/RemoteTaskCheckerManager.kt`
+
+It:
+
+1. Collects all registered remote checkers.
+2. Filters them by `canCheck(project, task)`.
+3. Returns exactly one checker or `null`.
+4. Throws if more than one checker matches.
+
+### Hyperskill remote checker
+
+Hyperskill registers:
+
+```xml
+
+```
+
+Implementation:
+
+`intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/checker/HyperskillRemoteTaskChecker.kt`
+
+It can check only when:
+
+- `task.course is HyperskillCourse`
+- `HyperskillCheckConnector.isRemotelyChecked(task)` is true
+
+That remote set is:
+
+- `CodeTask`
+- `RemoteEduTask`
+- `UnsupportedTask`
+
+### Hyperskill remote task behavior by type
+
+#### CodeTask
+
+Path:
+
+- `HyperskillRemoteTaskChecker.check(...)`
+- `HyperskillCheckConnector.checkCodeTask(...)`
+
+Behavior:
+
+1. Validates that task id exists.
+2. Tries websocket-based check session.
+3. If websocket path fails, falls back to HTTP submission.
+4. Polls the submission until status changes from `evaluation`.
+
+#### RemoteEduTask
+
+Path:
+
+- `HyperskillRemoteTaskChecker.check(...)`
+- `HyperskillCheckConnector.checkRemoteEduTask(...)`
+
+Behavior:
+
+1. Validates that task id exists.
+2. Collects solution files.
+3. Creates an attempt.
+4. Creates and posts a submission.
+5. Polls until final status is received.
+
+#### UnsupportedTask
+
+Path:
+
+- `HyperskillRemoteTaskChecker.check(...)`
+- `HyperskillCheckConnector.checkUnsupportedTask(...)`
+
+Behavior:
+
+1. Does not create a new submission.
+2. Loads existing submissions from the platform.
+3. Derives solved/failed state from the latest known submissions.
+
+### Hyperskill local EduTask behavior
+
+Hyperskill `EduTask` is special:
+
+- It is checked locally.
+- It does not use the remote checker.
+- After the local result is finalized, `HyperskillCheckListener.afterCheck(...)` may asynchronously post the solution to Hyperskill.
+
+This listener is registered in `META-INF/Hyperskill.xml`.
+
+Implementation:
+
+`intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/checker/HyperskillCheckListener.kt`
+
+Behavior:
+
+- `beforeCheck(...)` updates Hyperskill metrics.
+- `afterCheck(...)` restarts metrics for unsolved current tasks.
+- For non-remote, non-theory Hyperskill tasks, it posts the local solution to Hyperskill in background if the user is logged in.
+
+This postback is not the same thing as remote checking. It is a follow-up side effect after local checking has already completed.
+
+### Result finalization
+
+When background execution succeeds, `StudyCheckTask.onSuccess()`:
+
+1. Stores `task.status = checkResult.status`
+2. Stores `task.feedback`
+3. Persists the task via `saveItem(task)`
+4. Calls checker lifecycle hooks
+5. Updates the tool window
+6. Refreshes course progress and project view
+7. Calls all `CheckListener.afterCheck(...)`
+
+### Error and cancel behavior
+
+If the background task is cancelled:
+
+- the tool window is reset to `readyToCheck()`
+
+If an exception happens:
+
+- refresh-token failure is converted to `failedToSubmit(...)`
+- everything else becomes generic `failedToCheck`
+
+### Practical summary
+
+For the common Hyperskill task classes:
+
+- `EduTask`: local tests first, then optional async postback to Hyperskill
+- `RemoteEduTask`: remote-only check
+- `CodeTask`: remote-only check
+- `UnsupportedTask`: remote state lookup only, no fresh submission
+
+This local-first and then maybe-remote pattern is the most important rule to keep in mind when debugging `CheckAction`.
diff --git a/documentation/TaskTypeDetermination.md b/documentation/TaskTypeDetermination.md
new file mode 100644
index 000000000..160ea94cb
--- /dev/null
+++ b/documentation/TaskTypeDetermination.md
@@ -0,0 +1,279 @@
+## Task Type Determination
+
+This document explains where task class selection happens and why a task becomes `EduTask`, `RemoteEduTask`, `CodeTask`, `TheoryTask`, or `UnsupportedTask`.
+
+There are two main scenarios:
+
+1. building tasks from Hyperskill/Stepik server responses
+2. loading tasks from serialized local course data (JSON or YAML)
+
+### Two different type systems
+
+The codebase works with two related but distinct type concepts.
+
+#### Remote step block type
+
+This comes from Stepik/Hyperskill API and lives in `stepSource.block.name`.
+
+Typical values:
+
+- `pycharm`
+- `code`
+- `text`
+- other Stepik/Hyperskill values like `choice`, `sorting`, `matching`, and so on
+
+See:
+
+- `intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/StepikSteps.kt`
+- `hs-edu-format/src/org/hyperskill/academy/learning/courseFormat/hyperskill/HyperskillTaskType.kt`
+
+#### Plugin task type
+
+This is the internal task class identifier stored in `Task.itemType`.
+
+Typical values:
+
+- `edu`
+- `remote_edu`
+- `code`
+- `theory`
+- `unsupported`
+
+These values determine which Kotlin task class is instantiated during deserialization and how checking behaves later.
+
+### Where runtime Hyperskill tasks are determined
+
+The main runtime decision point is:
+
+`intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/courseGeneration/HyperskillTaskBuilder.kt`
+
+The crucial method is `build()`:
+
+```kotlin
+fun build(): Task? = when (val blockName = stepSource.block?.name) {
+ EduTask.PYCHARM_TASK_TYPE if stepSource.isRemoteTested -> createTask(RemoteEduTask.REMOTE_EDU_TASK_TYPE)
+ EduTask.PYCHARM_TASK_TYPE,
+ "text",
+ CodeTask.CODE_TASK_TYPE -> createTask(blockName)
+ else -> null
+}
+```
+
+This means:
+
+- `block.name == "pycharm"` and `is_remote_tested == true` -> `RemoteEduTask`
+- `block.name == "pycharm"` and `is_remote_tested == false` -> continue as plain `pycharm`
+- `block.name == "code"` -> `CodeTask`
+- `block.name == "text"` -> `TheoryTask`
+
+### Why `pycharm` often becomes `EduTask`
+
+The next step is delegation to:
+
+`intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/StepikTaskBuilder.kt`
+
+`HyperskillTaskBuilder.createTask(type)` calls `super.createTask(type)`.
+
+Inside `StepikTaskBuilder` there are two layers:
+
+1. mapping remote Stepik/Hyperskill task names to builders
+2. mapping plugin task type strings to actual task classes
+
+Important pieces:
+
+- `stepikTaskBuilders`
+- `pluginTaskTypes`
+- `pycharmTask(type: String? = null)`
+
+For `pycharm`, the builder goes into `pycharmTask()`:
+
+```kotlin
+HyperskillTaskType.PYCHARM -> { _: String -> pycharmTask() }
+```
+
+Then:
+
+```kotlin
+val taskType = type ?: stepOptions.taskType
+val task = pluginTaskTypes[taskType]?.invoke(taskName)
+ ?: EduTask(taskName, stepId, stepPosition, updateDate, CheckStatus.Unchecked)
+```
+
+This fallback is the key reason many programming tasks become `EduTask`.
+
+If:
+
+- no explicit override type was passed, and
+- `stepOptions.taskType` is missing, or
+- `stepOptions.taskType` is `pycharm`
+
+then `pluginTaskTypes[taskType]` does not resolve to a dedicated class, and the fallback is `EduTask`.
+
+### Why `RemoteEduTask` exists at all
+
+`RemoteEduTask` is a special subclass of `EduTask` used when a programming task looks like a `pycharm` task structurally, but Hyperskill wants it checked remotely instead of by local tests.
+
+That decision is made by:
+
+- `stepSource.block.name == "pycharm"`
+- `stepSource.isRemoteTested == true`
+
+See `HyperskillStepSource.isRemoteTested` in:
+
+`intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/api/hyperskillAPI.kt`
+
+This is the field that flips a regular programming-style task from local `EduTask` behavior into remote `RemoteEduTask` behavior.
+
+### How `checkProfile` gets attached
+
+Once the task instance is created, `HyperskillTaskBuilder.createTask(...)` fills Hyperskill-specific properties:
+
+```kotlin
+is EduTask -> {
+ if (task is RemoteEduTask) {
+ task.checkProfile = stepSource.checkProfile
+ }
+ name = stepSource.title
+}
+```
+
+So `checkProfile` is not what decides the class. It is attached after the class has already been decided.
+
+### CodeTask path
+
+`CodeTask` is chosen when the server step block type is exactly `code`.
+
+That is a separate family from `pycharm` tasks:
+
+- `code` -> `CodeTask`
+- `pycharm` -> `EduTask` or `RemoteEduTask`
+
+This distinction matters for checking:
+
+- `CodeTask` is remote-checked on Hyperskill
+- `EduTask` is locally checked
+- `RemoteEduTask` is remote-checked
+
+### TheoryTask path
+
+`TheoryTask` is chosen when `block.name == "text"`.
+
+That is handled by passing `"text"` into `StepikTaskBuilder.createTask(...)`, which maps through `HyperskillTaskType.TEXT` to `pycharmTask(THEORY_TASK_TYPE)`.
+
+### UnsupportedTask path
+
+In generic Stepik/Hyperskill mapping, unsupported step types are represented as `UnsupportedTask`.
+
+The fallback builder is:
+
+```kotlin
+else -> this::unsupportedTask
+```
+
+However, note that `HyperskillTaskBuilder.build()` itself currently filters accepted `block.name` values quite aggressively and may return `null` instead of building unsupported tasks in some paths. The generic `StepikTaskBuilder` still contains the broader unsupported-type behavior.
+
+### Where serialized type is stored
+
+Every task class defines its own `itemType`.
+
+Examples:
+
+- `EduTask.itemType = "edu"`
+- `RemoteEduTask.itemType = "remote_edu"`
+- `CodeTask.itemType = "code"`
+
+Files:
+
+- `hs-edu-format/src/org/hyperskill/academy/learning/courseFormat/tasks/EduTask.kt`
+- `hs-edu-format/src/org/hyperskill/academy/learning/courseFormat/tasks/RemoteEduTask.kt`
+- `hs-edu-format/src/org/hyperskill/academy/learning/courseFormat/tasks/CodeTask.kt`
+
+When task metadata is serialized into Stepik-style step options, `PyCharmStepOptions.taskType` is populated from `task.itemType`:
+
+```kotlin
+taskType = task.itemType
+```
+
+See:
+
+`intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/StepikSteps.kt`
+
+### JSON deserialization path
+
+When loading local JSON, task type is determined directly from serialized `task_type`, not from `block.name`.
+
+See:
+
+`hs-edu-format/src/org/hyperskill/academy/learning/json/mixins/LocalEduCourseMixins.kt`
+
+The key dispatch function is `deserializeTask(...)`.
+
+Mapping:
+
+- `ide` -> `IdeTask`
+- `theory` -> `TheoryTask`
+- `code` -> `CodeTask`
+- `edu` or deprecated `pycharm` -> `EduTask`
+- `output` -> `OutputTask`
+- `remote_edu` -> `RemoteEduTask`
+- `unsupported` -> `UnsupportedTask`
+
+The special compatibility rule is:
+
+```kotlin
+EduTask.EDU_TASK_TYPE, EduTask.PYCHARM_TASK_TYPE -> EduTask::class.java
+```
+
+So old saved `pycharm` tasks are intentionally loaded as `EduTask`.
+
+### YAML deserialization path
+
+When loading YAML, task class is chosen from the `type` field.
+
+See:
+
+`hs-edu-format/src/org/hyperskill/academy/learning/yaml/YamlDeserializer.kt`
+
+Mapping:
+
+- `edu` -> `EduTask`
+- `remote_edu` -> `RemoteEduTask`
+- `output` -> `OutputTask`
+- `theory` -> `TheoryTask`
+- `ide` -> `IdeTask`
+- `code` -> `CodeTask`
+- `unsupported` -> `UnsupportedTask`
+
+Again, this is direct class selection from serialized data, not inference from remote API step structure.
+
+### Practical rules
+
+When debugging “why is this task an `EduTask`?” use these rules:
+
+#### If the task came from Hyperskill API:
+
+- `block.name == "pycharm"` and `is_remote_tested == false` -> `EduTask`
+- `block.name == "pycharm"` and `is_remote_tested == true` -> `RemoteEduTask`
+- `block.name == "code"` -> `CodeTask`
+- `block.name == "text"` -> `TheoryTask`
+
+#### If the task came from saved JSON/YAML:
+
+- class comes from serialized type field directly
+- old `pycharm` serialized tasks are loaded as `EduTask`
+
+### Why this split exists
+
+Historically, `pycharm` was the generic programming-task format used by the plugin. Later, the plugin introduced internal task classes such as `EduTask` and `RemoteEduTask` with different checking semantics.
+
+As a result:
+
+- remote API payloads still often use `pycharm`
+- internal model prefers `edu` / `remote_edu`
+- deserializers keep backward compatibility with older `pycharm` task records
+
+That is why task type determination can look inconsistent until you separate:
+
+1. server block type
+2. internal task class type
+3. serialized local task type
diff --git a/hs-edu-format/src/org/hyperskill/academy/learning/courseFormat/tasks/RemoteEduTask.kt b/hs-edu-format/src/org/hyperskill/academy/learning/courseFormat/tasks/RemoteEduTask.kt
index 4194004ed..da9448722 100644
--- a/hs-edu-format/src/org/hyperskill/academy/learning/courseFormat/tasks/RemoteEduTask.kt
+++ b/hs-edu-format/src/org/hyperskill/academy/learning/courseFormat/tasks/RemoteEduTask.kt
@@ -8,6 +8,7 @@ import java.util.*
* By default, EduTask is checked with local tests, but if `is_remote_tested` flag is true, then we should send code to remote check
*/
// see EDU-4504 Support Go edu problems
+
class RemoteEduTask : EduTask {
constructor()
constructor(name: String, id: Int, position: Int, updateDate: Date, status: CheckStatus) : super(name, id, position, updateDate, status)
@@ -24,4 +25,4 @@ class RemoteEduTask : EduTask {
@NonNls
const val REMOTE_EDU_TASK_TYPE: String = "remote_edu"
}
-}
\ No newline at end of file
+}
diff --git a/hs-edu-format/src/org/hyperskill/academy/learning/yaml/YamlMapper.kt b/hs-edu-format/src/org/hyperskill/academy/learning/yaml/YamlMapper.kt
index 6e0200c76..bb05565d9 100644
--- a/hs-edu-format/src/org/hyperskill/academy/learning/yaml/YamlMapper.kt
+++ b/hs-edu-format/src/org/hyperskill/academy/learning/yaml/YamlMapper.kt
@@ -21,6 +21,7 @@ import org.hyperskill.academy.learning.courseFormat.hyperskill.HyperskillProject
import org.hyperskill.academy.learning.courseFormat.hyperskill.HyperskillStage
import org.hyperskill.academy.learning.courseFormat.hyperskill.HyperskillTopic
import org.hyperskill.academy.learning.courseFormat.tasks.CodeTask
+import org.hyperskill.academy.learning.courseFormat.tasks.RemoteEduTask
import org.hyperskill.academy.learning.courseFormat.tasks.Task
import org.hyperskill.academy.learning.courseFormat.tasks.TheoryTask
import org.hyperskill.academy.learning.yaml.format.*
@@ -31,6 +32,7 @@ import org.hyperskill.academy.learning.yaml.format.hyperskill.HyperskillStageMix
import org.hyperskill.academy.learning.yaml.format.hyperskill.HyperskillTopicMixin
import org.hyperskill.academy.learning.yaml.format.remote.DataTaskAttemptYamlMixin
import org.hyperskill.academy.learning.yaml.format.remote.RemoteCourseYamlMixin
+import org.hyperskill.academy.learning.yaml.format.remote.RemoteEduTaskYamlMixin
import org.hyperskill.academy.learning.yaml.format.remote.RemoteStudyItemYamlMixin
import org.hyperskill.academy.learning.yaml.format.student.FeedbackYamlMixin
import org.hyperskill.academy.learning.yaml.format.student.StudentTaskFileYamlMixin
@@ -87,6 +89,7 @@ object YamlMapper {
addMixIn(Lesson::class.java, LessonYamlMixin::class.java)
addMixIn(FrameworkLesson::class.java, FrameworkLessonYamlMixin::class.java)
addMixIn(Task::class.java, StudentTaskYamlMixin::class.java)
+ addMixIn(RemoteEduTask::class.java, RemoteEduTaskYamlMixin::class.java)
addMixIn(CodeTask::class.java, CodeTaskYamlMixin::class.java)
addMixIn(TheoryTask::class.java, TheoryTaskYamlUtil::class.java)
addMixIn(CheckFeedback::class.java, FeedbackYamlMixin::class.java)
diff --git a/hs-edu-format/src/org/hyperskill/academy/learning/yaml/format/remote/RemoteEduTaskYamlMixin.kt b/hs-edu-format/src/org/hyperskill/academy/learning/yaml/format/remote/RemoteEduTaskYamlMixin.kt
index 840b6c807..942b24646 100644
--- a/hs-edu-format/src/org/hyperskill/academy/learning/yaml/format/remote/RemoteEduTaskYamlMixin.kt
+++ b/hs-edu-format/src/org/hyperskill/academy/learning/yaml/format/remote/RemoteEduTaskYamlMixin.kt
@@ -1,5 +1,6 @@
package org.hyperskill.academy.learning.yaml.format.remote
+import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.annotation.JsonPropertyOrder
import org.hyperskill.academy.learning.courseFormat.EduFormatNames.CHECK_PROFILE
@@ -16,6 +17,8 @@ import org.hyperskill.academy.learning.yaml.format.student.StudentTaskYamlMixin
@Suppress("unused")
@JsonPropertyOrder(TYPE, CUSTOM_NAME, FILES, FEEDBACK_LINK, CHECK_PROFILE, STATUS, FEEDBACK, RECORD, TAGS)
class RemoteEduTaskYamlMixin : StudentTaskYamlMixin() {
- @JsonProperty(CHECK_PROFILE)
- private lateinit var checkProfile: String
-}
\ No newline at end of file
+ @get:JsonProperty(CHECK_PROFILE)
+ @set:JsonProperty(CHECK_PROFILE)
+ @get:JsonInclude(JsonInclude.Include.ALWAYS)
+ var checkProfile: String = ""
+}
diff --git a/hs-edu-format/src/org/hyperskill/academy/learning/yaml/format/tasks/TaskYamlUtil.kt b/hs-edu-format/src/org/hyperskill/academy/learning/yaml/format/tasks/TaskYamlUtil.kt
index 60ec3359f..428b65636 100644
--- a/hs-edu-format/src/org/hyperskill/academy/learning/yaml/format/tasks/TaskYamlUtil.kt
+++ b/hs-edu-format/src/org/hyperskill/academy/learning/yaml/format/tasks/TaskYamlUtil.kt
@@ -81,7 +81,7 @@ abstract class TaskYamlMixin {
protected open fun getAdditionalPropertiesForSerialization(): Map {
// Filter out known field names that are already serialized by the mixin
val knownFields = setOf("type", "custom_name", "files", "feedback_link",
- "solution_hidden", "status", "feedback", "record", "tags")
+ "solution_hidden", "status", "feedback", "record", "tags", "check_profile")
return additionalProperties.filterKeys { it !in knownFields }
}
diff --git a/intellij-plugin/hs-core/resources/messages/EduCoreBundle.properties b/intellij-plugin/hs-core/resources/messages/EduCoreBundle.properties
index ca5a61a8c..41ab28e79 100644
--- a/intellij-plugin/hs-core/resources/messages/EduCoreBundle.properties
+++ b/intellij-plugin/hs-core/resources/messages/EduCoreBundle.properties
@@ -244,6 +244,7 @@ error.failed.to.post.solution=Failed to post solution
error.failed.to.post.solution.to=Failed to post solution to {0}
error.failed.to.post.solution.with.guide=Failed to post solution to {0}. For more information, \
see the Troubleshooting guide
+hyperskill.error.empty.check.profile=Check profile is empty for task {0}. Please try to {1} or contact support.
error.failed.to.refresh.tokens=Failed to refresh tokens
error.invalid.rename.message=This rename operation can break the course
@@ -617,4 +618,4 @@ yaml.editor.notification.directory.not.found=Directory for item ''{0}'' was not
yaml.editor.notification.parameter.is.empty={0} is empty
# {0} for course YAML version, {1} for supported YAML version
yaml.version.compatibility.title=Course Created with Newer Plugin Version
-yaml.version.compatibility.message=This course was created with a newer version of the plugin (YAML version {0}). The current plugin supports YAML version {1}. The course will be loaded in compatibility mode, and the YAML version will be automatically downgraded. Some features from the newer plugin version may not be available.
\ No newline at end of file
+yaml.version.compatibility.message=This course was created with a newer version of the plugin (YAML version {0}). The current plugin supports YAML version {1}. The course will be loaded in compatibility mode, and the YAML version will be automatically downgraded. Some features from the newer plugin version may not be available.
diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/projectView/CourseViewUtils.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/projectView/CourseViewUtils.kt
index 999131757..7009a723f 100644
--- a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/projectView/CourseViewUtils.kt
+++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/projectView/CourseViewUtils.kt
@@ -6,6 +6,7 @@ import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VfsUtilCore.getRelativePath
+import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.*
import com.intellij.ui.LayeredIcon
import org.hyperskill.academy.EducationalCoreIcons.CourseView
@@ -57,8 +58,10 @@ object CourseViewUtils {
private fun isShowDirInView(project: Project, task: Task, dir: PsiDirectory): Boolean {
if (dir.children.isEmpty()) return true
val dirName = dir.name
- val hasTaskFileNotInsideSourceDir = task.hasVisibleTaskFilesNotInsideSourceDir(project)
- if (dirName == task.sourceDir) return hasTaskFileNotInsideSourceDir
+ if (dirName == task.sourceDir) {
+ val taskDir = dir.virtualFile.parent ?: return false
+ return task.hasVisibleTaskFilesNotInsideSourceDir(taskDir)
+ }
return task.taskFiles.values.any {
if (!it.isVisible || isTestFile(task, it.name)) return@any false
val virtualFile = it.getVirtualFile(project) ?: return@any false
@@ -66,12 +69,11 @@ object CourseViewUtils {
}
}
- private fun Task.hasVisibleTaskFilesNotInsideSourceDir(project: Project): Boolean {
- val taskDir = getDir(project.courseDir) ?: error("Directory for task $name not found")
+ private fun Task.hasVisibleTaskFilesNotInsideSourceDir(taskDir: VirtualFile): Boolean {
val sourceDir = findSourceDir(taskDir) ?: return false
return taskFiles.values.any {
if (!it.isVisible || isTestFile(this, it.name)) return@any false
- val virtualFile = it.getVirtualFile(project)
+ val virtualFile = it.findTaskFileInDir(taskDir)
if (virtualFile == null) {
Logger.getInstance(Task::class.java).warn("VirtualFile for ${it.name} not found")
return@any false
@@ -98,7 +100,7 @@ object CourseViewUtils {
val vFile = baseDir.virtualFile
val sourceVFile = vFile.findFileByRelativePath(sourceDirName) ?: return baseDir
- if (task.hasVisibleTaskFilesNotInsideSourceDir(project)) {
+ if (task.hasVisibleTaskFilesNotInsideSourceDir(vFile)) {
return baseDir
}
return PsiManager.getInstance(project).findDirectory(sourceVFile)
diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/checker/HyperskillSubmitConnector.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/checker/HyperskillSubmitConnector.kt
index 643bccba6..3c96ab660 100644
--- a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/checker/HyperskillSubmitConnector.kt
+++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/checker/HyperskillSubmitConnector.kt
@@ -79,6 +79,16 @@ object HyperskillSubmitConnector {
}
fun submitRemoteEduTask(task: RemoteEduTask, files: List): Result {
+ LOG.debug("Submitting RemoteEduTask `${task.name}` (id=${task.id})")
+ if (task.checkProfile.isEmpty()) {
+ val message = EduCoreBundle.message(
+ "hyperskill.error.empty.check.profile",
+ task.name,
+ EduCoreBundle.message("hyperskill.action.synchronize.project")
+ )
+ LOG.error(message)
+ return Err(message)
+ }
val connector = HyperskillConnector.getInstance()
val attempt = connector.postAttempt(task).onError {
return Err(it)
@@ -89,4 +99,4 @@ object HyperskillSubmitConnector {
}
private val LOG: Logger = logger()
-}
\ No newline at end of file
+}
diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/newProjectUI/HyperskillSelectTrackPanel.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/newProjectUI/HyperskillSelectTrackPanel.kt
index d3afc4f16..7bc739966 100644
--- a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/newProjectUI/HyperskillSelectTrackPanel.kt
+++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/newProjectUI/HyperskillSelectTrackPanel.kt
@@ -42,7 +42,7 @@ class HyperskillSelectTrackPanel : JPanel(BorderLayout()) {
}
row {
button(EduCoreBundle.message("course.dialog.hyperskill.jba.on.hyperskill.select.course")) {
- BrowserUtil.open("https://hyperskill.org/courses")
+ BrowserUtil.open("https://hyperskill.org/courses?utm_source=plugin_ha&utm_medium=plugin&utm_campaign=ha")
val dialog = UIUtil.getParentOfType(DialogWrapperDialog::class.java, this@HyperskillSelectTrackPanel)
dialog?.dialogWrapper?.close(DialogWrapper.OK_EXIT_CODE)
}.applyToComponent {
diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/submissions/HyperskillSubmissionFactory.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/submissions/HyperskillSubmissionFactory.kt
index 574d5a6b0..ef81a9bea 100644
--- a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/submissions/HyperskillSubmissionFactory.kt
+++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/stepik/hyperskill/submissions/HyperskillSubmissionFactory.kt
@@ -20,16 +20,17 @@ object HyperskillSubmissionFactory {
fun createEduTaskSubmission(task: Task, attempt: Attempt, files: List, feedback: String): StepikBasedSubmission {
val reply = EduTaskReply()
- reply.feedback = Feedback(feedback)
reply.score = if (task.status == CheckStatus.Solved) "1" else "0"
reply.solution = files
+ reply.feedback = Feedback(feedback)
return StepikBasedSubmission(attempt, reply)
}
fun createRemoteEduTaskSubmission(task: RemoteEduTask, attempt: Attempt, files: List): StepikBasedSubmission {
val reply = EduTaskReply()
- reply.checkProfile = task.checkProfile
+ reply.score = if (task.status == CheckStatus.Solved) "1" else "0"
reply.solution = files
+ reply.checkProfile = task.checkProfile
return StepikBasedSubmission(attempt, reply)
}
-}
\ No newline at end of file
+}
diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/yaml/YamlDeepLoader.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/yaml/YamlDeepLoader.kt
index 14cd57948..dccdabe91 100644
--- a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/yaml/YamlDeepLoader.kt
+++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/yaml/YamlDeepLoader.kt
@@ -157,15 +157,23 @@ object YamlDeepLoader {
}
}
- @Throws(RemoteYamlLoadingException::class)
fun Course.loadRemoteInfoRecursively(project: Project) {
- loadRemoteInfo(project)
- sections.forEach { section -> section.loadRemoteInfo(project) }
+ fun StudyItem.loadRemoteInfoSafe(project: Project) {
+ try {
+ loadRemoteInfo(project)
+ }
+ catch (e: RemoteYamlLoadingException) {
+ LOG.warn(e)
+ }
+ }
+
+ loadRemoteInfoSafe(project)
+ sections.forEach { section -> section.loadRemoteInfoSafe(project) }
// top-level and from sections
visitLessons { lesson ->
- lesson.loadRemoteInfo(project)
- lesson.taskList.forEach { task -> task.loadRemoteInfo(project) }
+ lesson.loadRemoteInfoSafe(project)
+ lesson.taskList.forEach { task -> task.loadRemoteInfoSafe(project) }
}
}
diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/yaml/YamlFormatSynchronizer.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/yaml/YamlFormatSynchronizer.kt
index cadc504ca..0c02aa1b2 100644
--- a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/yaml/YamlFormatSynchronizer.kt
+++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/yaml/YamlFormatSynchronizer.kt
@@ -146,13 +146,7 @@ object YamlFormatSynchronizer {
}
private fun StudyItem.saveConfig(project: Project, configName: String, mapper: ObjectMapper) {
- val dir = try {
- getConfigDir(project)
- }
- catch (e: IllegalStateException) {
- // Config dir not found - item was probably deleted from filesystem
- return
- }
+ val dir = getConfigDir(project) ?: return
val configFile = runReadAction { dir.findChild(configName) }
if (configFile?.getUserData(SAVE_TO_CONFIG) == false) return
@@ -263,4 +257,4 @@ private fun Course.disambiguateAdditionalFilesContents(project: Project) {
additionalFile.contents = disambiguatedContents
}
-}
\ No newline at end of file
+}
diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/yaml/YamlLoader.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/yaml/YamlLoader.kt
index a0b9f7f05..14f41382c 100644
--- a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/yaml/YamlLoader.kt
+++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/yaml/YamlLoader.kt
@@ -2,6 +2,7 @@ package org.hyperskill.academy.learning.yaml
import com.fasterxml.jackson.databind.ObjectMapper
import com.google.common.annotations.VisibleForTesting
+import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
@@ -11,7 +12,9 @@ import org.hyperskill.academy.learning.courseFormat.*
import org.hyperskill.academy.learning.courseFormat.ext.customContentPath
import org.hyperskill.academy.learning.courseFormat.ext.getDir
import org.hyperskill.academy.learning.courseFormat.ext.getPathToChildren
+import org.hyperskill.academy.learning.courseFormat.hyperskill.HyperskillCourse
import org.hyperskill.academy.learning.courseFormat.tasks.Task
+import org.hyperskill.academy.learning.courseFormat.tasks.UnsupportedTask
import org.hyperskill.academy.learning.messages.EduCoreBundle
import org.hyperskill.academy.learning.storage.persistEduFiles
import org.hyperskill.academy.learning.yaml.YamlConfigSettings.configFileName
@@ -30,8 +33,12 @@ import org.jetbrains.annotations.NonNls
* Uses [deserializeItemProcessingErrors] to deserialize object, than applies changes to existing object, see [loadItem].
*/
object YamlLoader {
+ @PublishedApi
+ internal val LOG = Logger.getInstance(YamlLoader::class.java)
+
@NonNls
private const val TOPIC = "Loaded YAML"
+
val YAML_LOAD_TOPIC: Topic = Topic.create(TOPIC, YamlListener::class.java)
fun loadItem(project: Project, configFile: VirtualFile, loadFromVFile: Boolean) {
@@ -89,7 +96,7 @@ object YamlLoader {
}
}
- inline fun StudyItem.deserializeContent(
+ internal inline fun StudyItem.deserializeContent(
project: Project,
contentList: List,
mapper: ObjectMapper = basicMapper(),
@@ -98,6 +105,12 @@ object YamlLoader {
for (titledItem in contentList) {
val configFile: VirtualFile = getConfigFileForChild(project, titledItem.name) ?: continue
val deserializeItem = deserializeItemProcessingErrors(configFile, project, mapper = mapper, parentItem = this) as? T ?: continue
+ if (this is Lesson && isHyperskillTopicsLesson() && deserializeItem is UnsupportedTask) {
+ LOG.warn(
+ "Skipping unsupported task `${titledItem.name}` while loading Hyperskill topic lesson `${name}` from ${configFile.path}"
+ )
+ continue
+ }
deserializeItem.name = titledItem.name
deserializeItem.index = titledItem.index
content.add(deserializeItem)
@@ -108,7 +121,7 @@ object YamlLoader {
fun StudyItem.getConfigFileForChild(project: Project, childName: String): VirtualFile? {
val courseDir = project.courseDir
- val dir = getDir(courseDir) ?: error(noDirForItemMessage(name))
+ val dir = getDir(courseDir) ?: return null
val itemDir = dir.findFileByRelativePath(getPathToChildren())?.findChild(childName)
val configFile = childrenConfigFileNames.map { itemDir?.findChild(it) }.firstOrNull { it != null }
@@ -217,6 +230,12 @@ object YamlLoader {
}
+@PublishedApi
+internal fun Lesson.isHyperskillTopicsLesson(): Boolean {
+ val course = course as? HyperskillCourse ?: return false
+ return (parentOrNull as? Section) === course.getTopicsSection()
+}
+
private fun StudyItem.ensureChildrenExist(itemDir: VirtualFile, customContentPath: String) {
when (this) {
is ItemContainer -> {
diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/yaml/format/student/StudentTaskChangeApplier.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/yaml/format/student/StudentTaskChangeApplier.kt
index c0baf37a4..bab9ea198 100644
--- a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/yaml/format/student/StudentTaskChangeApplier.kt
+++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/yaml/format/student/StudentTaskChangeApplier.kt
@@ -4,14 +4,12 @@ import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project
import org.hyperskill.academy.learning.courseFormat.StudyItem
import org.hyperskill.academy.learning.courseFormat.TaskFile
-import org.hyperskill.academy.learning.courseFormat.tasks.EduTask
import org.hyperskill.academy.learning.courseFormat.tasks.RemoteEduTask
import org.hyperskill.academy.learning.courseFormat.tasks.Task
import org.hyperskill.academy.learning.messages.EduCoreBundle
import org.hyperskill.academy.learning.yaml.errorHandling.YamlLoadingException
import org.hyperskill.academy.learning.yaml.format.TaskChangeApplier
-
class StudentTaskChangeApplier(project: Project) : TaskChangeApplier(project) {
override fun applyChanges(existingItem: Task, deserializedItem: Task) {
if (existingItem.solutionHidden != deserializedItem.solutionHidden && !ApplicationManager.getApplication().isInternal) {
@@ -24,11 +22,10 @@ class StudentTaskChangeApplier(project: Project) : TaskChangeApplier(project) {
existingItem.feedback = deserializedItem.feedback
// Note: record is no longer serialized (legacy field), don't overwrite existing value
- when (existingItem) {
- is EduTask -> {
- if (existingItem is RemoteEduTask) {
- existingItem.checkProfile = (deserializedItem as RemoteEduTask).checkProfile
- }
+ if (existingItem is RemoteEduTask && deserializedItem is RemoteEduTask) {
+ val newCheckProfile = deserializedItem.checkProfile
+ if (newCheckProfile.isNotEmpty() && newCheckProfile != existingItem.checkProfile) {
+ existingItem.checkProfile = newCheckProfile
}
}
}
@@ -41,4 +38,4 @@ class StudentTaskChangeApplier(project: Project) : TaskChangeApplier(project) {
override fun changeType(project: Project, existingItem: StudyItem, deserializedItem: Task) {
throw YamlLoadingException(EduCoreBundle.message("yaml.editor.invalid.not.allowed.to.change.task"))
}
-}
\ No newline at end of file
+}
diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/yaml/utils.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/yaml/utils.kt
index 05c9babfd..2bf4c8926 100644
--- a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/yaml/utils.kt
+++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/yaml/utils.kt
@@ -16,11 +16,10 @@ import org.hyperskill.academy.learning.yaml.YamlConfigSettings.remoteConfigFileN
* In most cases it's the same as [getDir] except the case when it's [Task] inside [FrameworkLesson] in student mode.
* In this case, all meta files, including config ones, are stored separately
*/
-fun StudyItem.getConfigDir(project: Project): VirtualFile {
- val configDir = if (this is Task) getTaskDirectory(project) else getDir(project.courseDir)
- return configDir ?: error("Config dir for `$name` not found")
+fun StudyItem.getConfigDir(project: Project): VirtualFile? {
+ return if (this is Task) getTaskDirectory(project) else getDir(project.courseDir)
}
fun StudyItem.remoteConfigFile(project: Project): VirtualFile? {
- return getConfigDir(project).findChild(remoteConfigFileName)
+ return getConfigDir(project)?.findChild(remoteConfigFileName)
}
diff --git a/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/coursecreator/yaml/YamlSerializationTest.kt b/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/coursecreator/yaml/YamlSerializationTest.kt
index 609b1eb1c..32f3e2d7c 100644
--- a/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/coursecreator/yaml/YamlSerializationTest.kt
+++ b/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/coursecreator/yaml/YamlSerializationTest.kt
@@ -50,6 +50,7 @@ class YamlSerializationTest : YamlTestCase() {
|- name: Main.go
| visible: true
| learner_created: false
+ |check_profile: $checkProfile
|status: Unchecked
|record: -1
|""".trimMargin()
diff --git a/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/courseView/NodesTest.kt b/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/courseView/NodesTest.kt
index 0c7a00b70..4e07c9f80 100644
--- a/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/courseView/NodesTest.kt
+++ b/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/courseView/NodesTest.kt
@@ -1,10 +1,14 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.hyperskill.academy.learning.courseView
+import com.intellij.psi.PsiManager
+import org.hyperskill.academy.learning.courseDir
import org.hyperskill.academy.learning.configurators.FakeGradleBasedLanguage
import org.hyperskill.academy.learning.courseFormat.CheckStatus
import org.hyperskill.academy.learning.courseFormat.CourseMode
+import org.hyperskill.academy.learning.courseFormat.ext.getDir
import org.hyperskill.academy.learning.courseFormat.hyperskill.HyperskillCourse
+import org.hyperskill.academy.learning.projectView.CourseViewUtils
import org.junit.Test
class NodesTest : CourseViewTestBase() {
@@ -41,6 +45,27 @@ class NodesTest : CourseViewTestBase() {
)
}
+ @Test
+ fun `test task directory lookup does not require task getDir`() {
+ courseWithFiles(language = FakeGradleBasedLanguage) {
+ lesson {
+ eduTask {
+ taskFile("src/file.txt")
+ }
+ }
+ }
+
+ val task = findTask(0, 0)
+ val taskDir = task.getDir(project.courseDir)!!
+ val taskPsiDir = PsiManager.getInstance(project).findDirectory(taskDir)!!
+
+ task.name = "missingTask"
+ assertNull(task.getDir(project.courseDir))
+
+ val directory = CourseViewUtils.findTaskDirectory(project, taskPsiDir, task)
+ assertEquals("src", directory?.name)
+ }
+
@Test
fun testSections() {
courseWithFiles {
diff --git a/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/format/yaml/StudentChangeApplierTest.kt b/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/format/yaml/StudentChangeApplierTest.kt
index c9c40bc9e..0721a44e6 100644
--- a/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/format/yaml/StudentChangeApplierTest.kt
+++ b/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/format/yaml/StudentChangeApplierTest.kt
@@ -2,6 +2,7 @@ package org.hyperskill.academy.learning.format.yaml
import org.hyperskill.academy.learning.courseFormat.TaskFile
import org.hyperskill.academy.learning.courseFormat.tasks.EduTask
+import org.hyperskill.academy.learning.courseFormat.tasks.RemoteEduTask
import org.hyperskill.academy.learning.yaml.YamlTestCase
import org.hyperskill.academy.learning.yaml.format.getChangeApplierForItem
import org.junit.Test
@@ -60,4 +61,38 @@ class StudentChangeApplierTest : YamlTestCase() {
assertEquals(deserializedItem.taskFiles.values.first().text, existingItem.taskFiles.values.first().text)
}
-}
\ No newline at end of file
+
+ @Test
+ fun `test remote edu task keeps existing check profile when deserialized value is empty`() {
+ val existingItem = courseWithFiles {
+ lesson {
+ remoteEduTask("task1", checkProfile = "hyperskill_go")
+ }
+ }.lessons.first().taskList.first() as RemoteEduTask
+ val deserializedItem = RemoteEduTask().apply {
+ name = "task1"
+ checkProfile = ""
+ }
+
+ getChangeApplierForItem(project, existingItem).applyChanges(existingItem, deserializedItem)
+
+ assertEquals("hyperskill_go", existingItem.checkProfile)
+ }
+
+ @Test
+ fun `test remote edu task updates check profile when deserialized value is non empty`() {
+ val existingItem = courseWithFiles {
+ lesson {
+ remoteEduTask("task1", checkProfile = "hyperskill_go")
+ }
+ }.lessons.first().taskList.first() as RemoteEduTask
+ val deserializedItem = RemoteEduTask().apply {
+ name = "task1"
+ checkProfile = "hyperskill_kotlin"
+ }
+
+ getChangeApplierForItem(project, existingItem).applyChanges(existingItem, deserializedItem)
+
+ assertEquals("hyperskill_kotlin", existingItem.checkProfile)
+ }
+}
diff --git a/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillCreateSubmissionTest.kt b/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillCreateSubmissionTest.kt
index af71b5050..546b60388 100644
--- a/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillCreateSubmissionTest.kt
+++ b/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/stepik/hyperskill/HyperskillCreateSubmissionTest.kt
@@ -100,6 +100,7 @@ class HyperskillCreateSubmissionTest : EduTestCase() {
|attempt: 12345
|reply:
| version: $JSON_FORMAT_VERSION
+ | score: ""
| solution:
| - name: src/Task.kt
| is_visible: true
@@ -134,4 +135,4 @@ class HyperskillCreateSubmissionTest : EduTestCase() {
val actual = YamlMapper.basicMapper().writeValueAsString(submission)
assertEquals(expected, actual)
}
-}
\ No newline at end of file
+}
diff --git a/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/yaml/YamlChangedAfterEventTest.kt b/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/yaml/YamlChangedAfterEventTest.kt
index 2b0609672..f4ba3f56c 100644
--- a/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/yaml/YamlChangedAfterEventTest.kt
+++ b/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/yaml/YamlChangedAfterEventTest.kt
@@ -52,7 +52,7 @@ class YamlChangedAfterEventTest : YamlTestCase() {
private fun checkConfig(item: StudyItem, expectedConfig: String) {
UIUtil.dispatchAllInvocationEvents()
- val configFile = item.getConfigDir(project).findChild(item.configFileName)
+ val configFile = item.getConfigDir(project)?.findChild(item.configFileName)
?: error("No config file for item: ${item::class.simpleName} ${item.name}")
val document = FileDocumentManager.getInstance().getDocument(configFile)!!