{"record":{"id":"4f2ed4e7a13d4f22","repo":"overleaf/overleaf","slug":"cannot-switch-an-archived-split-test-to-next-phase","errorCode":null,"errorMessage":"Cannot switch an archived split test to next phase","messagePattern":"Cannot switch an archived split test to next phase","errorType":"http","errorClass":"InvalidError","httpStatus":400,"severity":"error","filePath":"services/web/app/src/Features/SplitTests/SplitTestManager.mjs","lineNumber":237,"sourceCode":"  } catch (error) {\n    throw OError.tag(error, 'Failed to merge all split tests, merged set was', {\n      merged,\n    })\n  }\n}\n\nasync function switchToNextPhase(\n  { name, comment, targetPhase, labsUserLimit, clearUserLimit },\n  userId\n) {\n  const splitTest = await getSplitTest({ name })\n  if (!splitTest) {\n    throw new Errors.NotFoundError(\n      `Cannot switch split test with ID '${name}' to next phase: not found`\n    )\n  }\n  if (splitTest.archived) {\n    throw new Errors.InvalidError(\n      'Cannot switch an archived split test to next phase',\n      {\n        name,\n      }\n    )\n  }\n  const lastVersionCopy = SplitTestUtils.getCurrentVersion(splitTest).toObject()\n  lastVersionCopy.versionNumber++\n\n  const currentPhase = lastVersionCopy.phase\n\n  // Determine and validate target phase\n  if (targetPhase) {\n    const validTransitions = {\n      [ALPHA_PHASE]: [LABS_PHASE, BETA_PHASE],\n      [LABS_PHASE]: [BETA_PHASE],\n      [BETA_PHASE]: [RELEASE_PHASE],\n    }","sourceCodeStart":219,"sourceCodeEnd":255,"githubUrl":"https://github.com/overleaf/overleaf/blob/28ad3b03b71cb4311decdcb55c36b33ec10d72db/services/web/app/src/Features/SplitTests/SplitTestManager.mjs#L219-L255","documentation":"Thrown by switchToNextPhase in SplitTestManager.mjs when the split test named in the request exists but has the `archived` flag set. Archived tests are treated as finished/retired and the manager refuses any lifecycle mutation, including phase switches. It surfaces as an InvalidError (HTTP 400) so callers know the request is well-formed but the target state forbids it.","triggerScenarios":"Calling switchToNextPhase({ name, ... }) where getSplitTest({ name }) resolves a test whose `archived` property is true. Happens via the admin phase-switch endpoint on an archived test, or scripted automation iterating over all tests including archived ones.","commonSituations":"An admin UI list that does not filter out archived tests; a batch script advancing every test to release; a test archived earlier (e.g. after release) that someone tries to re-advance; stale local data where the test was archived by another operator.","solutions":["Fetch the test first and check `archived` before calling switchToNextPhase; skip archived tests in batch jobs.","Unarchive the test (clear the `archived` flag) if it genuinely needs to be reactivated, then retry the phase switch.","Filter admin UI / API queries with archived=false so archived tests cannot be selected."],"exampleFix":"// before\nawait splitTestManager.promises.switchToNextPhase({ name: 'my-test' })\n// after\nconst test = await splitTestManager.promises.getSplitTest({ name: 'my-test' })\nif (!test.archived) {\n  await splitTestManager.promises.switchToNextPhase({ name: 'my-test' })\n}","handlingStrategy":"validation","validationCode":"const test = await splitTestManager.promises.getSplitTest({ name })\nif (!test || test.archived) throw new Error(`test '${name}' missing or archived`)","typeGuard":"function canSwitchPhase(test) {\n  return test != null && test.archived === false\n}","tryCatchPattern":"try {\n  await switchToNextPhase({ name })\n} catch (err) {\n  if (err instanceof Errors.InvalidError && /archived/.test(err.message)) {\n    // skip or unarchive\n  } else throw err\n}","preventionTips":["Filter archived: false in any list used to drive phase switches","Pre-fetch the test and check the archived flag before mutating","Disable advance/revert actions in admin UI for archived tests"],"tags":["split-test","invalid-state","archived"],"backgroundTag":"operation-on-archived-resource","analyzedSha":"28ad3b03b71cb4311decdcb55c36b33ec10d72db","analyzedAt":"2026-09-03T02:10:22.807Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T07:17:11.731Z"}