Skip to content

[ALT-10963] Change check_profile , opening project view.#39

Open
KirillovProj wants to merge 6 commits into
mainfrom
bug/ALT-10963_empty_check_profile
Open

[ALT-10963] Change check_profile , opening project view.#39
KirillovProj wants to merge 6 commits into
mainfrom
bug/ALT-10963_empty_check_profile

Conversation

@KirillovProj
Copy link
Copy Markdown

@KirillovProj KirillovProj commented Apr 29, 2026

Summary

  • preserve check_profile for remote tasks during YAML serialization/deserialization and student task updates, so server data is not overwritten with an empty value
  • harden Hyperskill project opening and YAML loading by tolerating missing config directories, skipping unsupported topic tasks, and handling remote YAML loading failures per item instead of aborting the whole load
  • fix task directory resolution in the course view so projects still open correctly when Task.getDir() cannot resolve the task directory
  • block remote task submission when check_profile is empty and add logging around load/apply/save/submit flows to make these failures diagnosable

Details

  • added a dedicated YAML mixin for RemoteEduTask and explicitly serialize check_profile
  • excluded check_profile from generic additional-property serialization to avoid duplicate/conflicting YAML output
  • changed student task change application to update check_profile only for remote tasks and log mismatches
  • made config-dir lookups nullable and updated YAML sync/load code to fail soft when files or directories are missing
  • updated deep remote info loading to continue after RemoteYamlLoadingException
  • adjusted course view logic to resolve visible task files from the actual task directory instead of relying on a later getDir() lookup
  • added regression coverage for YAML serialization, YAML change handling, and course view task directory lookup
  • updated the Hyperskill courses link in the new project UI to use the tracked campaign URL

Testing

  • added/updated regression tests for:
    • remote task YAML serialization
    • YAML change-after-event handling
    • course view task directory lookup

@KirillovProj KirillovProj requested a review from Tsyklop April 29, 2026 14:14
@KirillovProj KirillovProj self-assigned this Apr 29, 2026
@KirillovProj KirillovProj added the bug Something isn't working label Apr 29, 2026
@Tsyklop Tsyklop changed the title [ALT-10963] Don't overwrite check_profile from server with an empty string. [ALT-10963] Change check_profile , opening project view. May 12, 2026
Copy link
Copy Markdown
Contributor

@meanmail meanmail left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

The PR has grown well beyond its original one-line scope. The original empty-string-overwrite fix from commit 3766a0d was lost in subsequent commits, so the very bug this PR was created for is no longer prevented. There are also several leftover debug logs, unused imports, scope creep (UI URL change, project view refactor, soft-fail YAML loading, Retrofit body logging, two large docs files), and a few smaller correctness/style issues. Details inline.

Blocking

  1. StudentTaskChangeApplier still overwrites a non-empty checkProfile with an empty value when the deserialized item carries "". The newCheckProfile.isNotEmpty() guard from 3766a0d was dropped in e44e4073. Please restore it and add a regression test covering exactly this scenario.

Strongly recommended before merge

  1. Strip the [DEBUG_LOG]-prefixed and per-call info logs (model setter, every YAML save, every applyChanges, every Retrofit request body, every deserialization). At minimum downgrade them to debugLOG.info here will produce a lot of noise on real courses and the Retrofit body log can leak auth-bearing payloads.
  2. Drop unused imports in YamlLoader.kt (EduOAuthCodeFlowConnector, HTTP_BAD_REQUEST, HTTP_FORBIDDEN, HTTP_UNAUTHORIZED).
  3. Consider splitting the unrelated changes (course-view refactor, UTM-link change, soft-fail in YamlDeepLoader, Retrofit body logging, the two new docs files) into separate PRs — they are not part of ALT-10963 and make review harder.

Smaller things

  1. isHyperskillTopicsLesson() matches section.presentableName == "Topics" — fragile; reuse HyperskillCourse.getTopicsSection() and compare by reference.
  2. HyperskillSubmitConnector user-facing error string is hardcoded English instead of EduCoreBundle.
  3. HyperskillSubmissionFactory.createEduTaskSubmission now explicitly assigns reply.checkProfile = "". EduTask has no check profile concept — please confirm the server expects this field on plain edu submissions; otherwise drop the assignment.
  4. RemoteEduTaskYamlMixin.kt is missing a final newline; two blank lines before private val LOG in RemoteEduTask.kt.

val newCheckProfile = deserializedItem.checkProfile
if (newCheckProfile != existingItem.checkProfile) {
LOG.info("Updating checkProfile for task ${existingItem.name}: '${existingItem.checkProfile}' -> '$newCheckProfile'")
existingItem.checkProfile = newCheckProfile
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking — the original bug is back.

The original fix in commit 3766a0d was if (newCheckProfile.isNotEmpty()) existingItem.checkProfile = newCheckProfile. Commit e44e4073 replaced that with a plain inequality check, so now if the deserialized YAML carries check_profile: '' while the in-memory task has a real profile (e.g. "hyperskill_go"), this line still overwrites the existing value with an empty string — the exact scenario ALT-10963 is supposed to prevent. The LOG.warn("checkProfile is empty … after applying changes") below is essentially logging the bug rather than preventing it.

Suggested fix: bring back the guard, e.g.

if (newCheckProfile.isNotEmpty() && newCheckProfile != existingItem.checkProfile) {
  existingItem.checkProfile = newCheckProfile
}

and add a regression test that loads YAML with an empty check_profile over a RemoteEduTask whose checkProfile was already set.

override fun applyChanges(existingItem: Task, deserializedItem: Task) {
LOG.info("Applying changes for task: ${existingItem.name} (id=${existingItem.id})")
LOG.info("Existing item checkProfile: ${(existingItem as? RemoteEduTask)?.checkProfile}")
LOG.info("Deserialized item checkProfile: ${(deserializedItem as? RemoteEduTask)?.checkProfile}")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three LOG.info lines per applyChanges call will be very noisy on real courses (this runs for every YAML change event). Please downgrade to debug or drop them — the same information is already logged on the setter and on save.

existingItem.checkProfile = newCheckProfile
}
if (existingItem.checkProfile.isEmpty()) {
LOG.warn("checkProfile is empty for RemoteEduTask ${existingItem.name} after applying changes")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once the empty-overwrite guard from line 35 is restored, this warning becomes unreachable / misleading. Either remove it, or replace it with a hard failure (throw YamlLoadingException(...)) so we don't silently keep a task in a broken state.

var checkProfile: String = ""
set(value) {
if (field != value) {
LOG.info("[DEBUG_LOG] RemoteEduTask.checkProfile set to '$value' (was '$field')")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting a logging side-effect on a domain-model property setter (and with a [DEBUG_LOG] prefix) reads like leftover debugging instrumentation. If we need to trace when checkProfile changes, do it at the call sites (loader / change applier) rather than the model, and at debug level.

*/
// see EDU-4504 Support Go edu problems


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: two consecutive blank lines between the type-level comment and private val LOG.

if (task.checkProfile.isEmpty()) {
val message = "Check profile is empty for task ${task.name}. Submission aborted to avoid server error."
LOG.error(message)
return Err(message + " Please try to 'Synchronize project' or contact support.")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User-facing string is hardcoded in English here. Other error strings in this file go through EduCoreBundle.message(...) — please follow the same convention so the message stays translatable.

}

fun submitRemoteEduTask(task: RemoteEduTask, files: List<SolutionFile>): Result<StepikBasedSubmission, String> {
LOG.info("Submitting RemoteEduTask: ${task.name} (id=${task.id}), checkProfile='${task.checkProfile}'")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logging every submission at info is fine for now, but please consider whether debug would be enough — every check action will land here.

reply.score = if (task.status == CheckStatus.Solved) "1" else "0"
reply.solution = files
reply.feedback = Feedback(feedback)
reply.checkProfile = ""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createEduTaskSubmission is for plain EduTask, which has no notion of checkProfile. Forcing reply.checkProfile = "" here sends an empty check_profile to the server for every regular edu submission — please confirm the server tolerates it; otherwise drop the assignment. If it is needed only to satisfy some serialization contract, a comment explaining why would help.

if (newRequest.body != null) {
val buffer = Buffer()
runCatching { newRequest.body!!.writeTo(buffer) }
.onSuccess { LOG.info("Retrofit request body for ${newRequest.method} ${newRequest.url}: ${buffer.readUtf8()}") }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logging the full request body at info for every Retrofit call is risky: it will (a) flood idea.log and (b) potentially log payloads that include tokens/PII (this interceptor runs after the auth interceptors). At minimum downgrade to debug and gate by a system property; ideally remove before merge.

@set:JsonProperty(CHECK_PROFILE)
@get:JsonInclude(JsonInclude.Include.ALWAYS)
var checkProfile: String = ""
} No newline at end of file
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: file is missing a final newline.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants