Class ExpressionExperimentServiceImplTest
- Author:
- daq2101, paul, poirigui
-
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionvoidvoidvoidfrinkbro's notes, rewritten as Python-serialized JSON on 2026-09-13, preflighted as an update on every experiment that carried one (GSE4036, GSE3489): the echo is compact and the comparison was by string.voidW10 โ two factor values under one factor whose statements serialize identically (distinguished only by a zygosity statement the curation side adds).voidW9 โ two factors sharing both category and name.voidSetting only a factor value's baseline flag is a real change: it must NOT be short-circuited as a no-op, must be written to the entity, and must be reflected in the rebuilt design returned by the apply.void๐ This service applies FULL-RECORD REPLACEMENT, so an omittedsupportingEvidenceCLEARS a stored one and counts as a change.voidThe evidence code replaces on the same terms as supporting evidence: an omitted one CLEARS a stored code and counts as a change.voidDesignating a baseline leaves its siblings alone.voidThe curator edits the subject and nothing else.voidAn evidence code is as invisible to the structural summary and tostatementsChangedas supporting evidence is, so a code-only edit needs the same dedicated check or it is swallowed as a no-op.voidAttaching evidence to an otherwise-unchanged statement leaves every content key identical, so the structural summary andstatementsChangedboth see nothing.voidW11 / W1 โ a factor category re-term with the factor values left in place.voidW11 โ a factor description-only edit is a real change.voidW11 โ same gap, reached through the factor name.voidW14 โ editing the measurement on a kept continuous factor value.voidvoidThe omission contract (Paul, 2026-09-13): on agemmaIdfactor item, a null field is no change.voidW16 โ non-ASCII in curated text survives the apply byte-exact.voidW8 โ a factor value carrying deliberately ungrounded free text (JAX strain nomenclature, hybrid backgrounds with no ontology term in existence) round-trips with the text intact.voidvoidThe other half of the same contract: the one excluded edit removes nothing.voidThe apply side executes the report's list instead of deriving its own.voidEditing only a kept factor value's statement (no structural add/delete) is a real change: it must not be short-circuited as a no-op, and the edit must reach the entity.voidAn evidence code stated on a factor-value statement reaches the entity, so a design write can say how it was decided.voidW13 โ supporting evidence attached to a factor-value statement reaches the entity.void๐ A design round-trip must not perform the parked term-URI migration.voidvoidA preflight computes the same tallies without writing, so it must not rebuild EE2C either โ the rebuild would publish annotations the dry run did not apply.voidA per-sample characteristic change reaches EE2C at the BioMaterial level and at no other.voidRe-terming a statement on a kept factor value reaches EE2C at the ExperimentalDesign level.voidA tag add/remove reaches EE2C at the ExpressionExperiment level and at no other.voidThe same refusal for a sample characteristic that belongs to no sample of the experiment.voidA tag delete that names nothing on the experiment is refused before anything writes, on the commit and on the dry run.voidA commit whose sections all matched what was already stored pays for no rebuild.voidvoidvoidW14 โ a newly created continuous factor value must keep its unit.void๐ A dropped factor is STILL IN its design's factor collection whenexperimentalFactorService.removeis called.voidStored evidence that is not JSON is served as nothing, so no client can echo it.voidvoidA factor MAY designate more than one baseline: a dataset holding two experiments has a reference level per experiment, and a curator has to be able to record that.voidOne of exactly the length the column holds is not refused -- the guard is off-by-one otherwise.voidAdding a factor deletes nothing: the existing analyses still test what they tested.voidvoidvoidvoidvoidvoidvoidA biomaterial that both loses a factor value in the payload and gains a deferred one from the plan is ONE changed biomaterial.void๐ A biomaterial bound to a factor value the commit CREATES cannot be seen in the design payload at all: an assignment carries factor value ids, and a new factor value has none until the first apply pass makes it.voidvoidvoidW3 โ deleting a continuous factor takes its measurement-bearing factor values with it, and the preflight says so before the caller commits.void๐ The shape an HTTP client actually round-trips.voidAnd the echo is a no-op: before the halves were re-joined, one entry became two content keys neither of which matched the stored statement, so an unedited round-trip counted as an edit on every compound statement in the design.voidvoidThe other side of that exemption: flagging a value other than the fitted reference still invalidates.void๐ Flagging the value the analysis was already fitted against moves no reference.voidA baseline flip moves no structural counter, so it rode through as "safe" and quietly falsified the analysis: the stored result set still names the OLD reference, and every contrast is relative to it.voidThe plan may name a biomaterial that is not part of this experiment.voidAn analysis on another factor survives a change to the treatment factor.voidvoidvoidA free-text value longer thanFACTOR_VALUE.VALUEis refused by the preflight rather than by the flush.voidHalf a first pair is refused on the design routes, which take the value objects directly.voidAnd half a second pair, which only PUT /curation refused before.voidA statement holds two objects and no more, so a third entry for one id is refused rather than dropped.voidThe exclusion, and the only one: relabelling a kept factor value does not move a sample, a level or a reference, so the analysis still describes what it described.voidThe re-type frinkbro preflighted on GSE93069 (2026-09-12): delete a categorical age factor and create a continuous one.voidThe factor half of the same gap: renaming a kept factor touches no factor value at all.voidA re-term of a kept factor value is a change the report has to be able to state.voidThe other half of the rule: a proposal that changes nothing reports nothing.voidAn analysis whose factors cannot be read is still deleted by a structural change: not knowing what it uses is not evidence that it uses nothing.voidRe-enabled 2026-08-16.voidA stranded subset requires the same explicit consent as the analysis cascade.voidA change with no analyses to delete and no stranded subsets proceeds without consent.voidvoid
-
Constructor Details
-
ExpressionExperimentServiceImplTest
public ExpressionExperimentServiceImplTest()
-
-
Method Details
-
setupMocks
@BeforeEach public void setupMocks() -
resetMocks
@AfterEach public void resetMocks() -
testExpressionExperimentFindAll
@Test public void testExpressionExperimentFindAll() -
testReplaceAllRawDataVectors
@Test public void testReplaceAllRawDataVectors() -
testReplaceAllRawDataVectorsWithMoreThanOnePreferredQt
@Test public void testReplaceAllRawDataVectorsWithMoreThanOnePreferredQt() -
testPreviewNoChange
@Test public void testPreviewNoChange() -
testPreviewDeletingAFactorValueFlagsDependentAnalyses
@Test public void testPreviewDeletingAFactorValueFlagsDependentAnalyses() -
testPreviewDeletingAFactorCascadesToItsValuesAndAnalyses
@Test public void testPreviewDeletingAFactorCascadesToItsValuesAndAnalyses() -
testPreviewReassigningBiomaterialFlagsAnalyses
@Test public void testPreviewReassigningBiomaterialFlagsAnalyses() -
testPreviewAddingNewFvFlagsAnalysesOnTheParentFactor
@Test public void testPreviewAddingNewFvFlagsAnalysesOnTheParentFactor() -
testPreviewCountsBiomaterialsBoundToAFactorValueTheCommitCreates
@Test public void testPreviewCountsBiomaterialsBoundToAFactorValueTheCommitCreates()๐ A biomaterial bound to a factor value the commit CREATES cannot be seen in the design payload at all: an assignment carries factor value ids, and a new factor value has none until the first apply pass makes it. Those bindings ride in theDesignCommitPlanand are attached by a second pass, so a preflight that ignores the plan reports zero changed biomaterials for a create whose bindings do land โ measured on GSE35977, where the preflight said 0 and the write bound 168 of 168 (cab, 2026-09-10). -
testPreviewCountsABiomaterialOnceWhenItBothLosesAndGainsAFactorValue
@Test public void testPreviewCountsABiomaterialOnceWhenItBothLosesAndGainsAFactorValue()A biomaterial that both loses a factor value in the payload and gains a deferred one from the plan is ONE changed biomaterial. Counting the two apply passes separately instead reported 240 changed biomaterials for GSE19804, an experiment with 120 samples (cab, 2026-09-10). -
testPreviewIgnoresADeferredBindingForAForeignBiomaterial
@Test public void testPreviewIgnoresADeferredBindingForAForeignBiomaterial()The plan may name a biomaterial that is not part of this experiment.buildAssignmentPassbinds nothing for such a biomaterial, so the prediction must not count it either. -
testPreviewEditingStatementOnKeptFvDoesNotFlagAnalyses
@Test public void testPreviewEditingStatementOnKeptFvDoesNotFlagAnalyses() -
testPreviewReportsAReTermOfAKeptFactorValue
@Test public void testPreviewReportsAReTermOfAKeptFactorValue()A re-term of a kept factor value is a change the report has to be able to state. It moves no structural counter โ same factor, same factor value, same samples โ and before 2026-08-28 the whole class of edit preflighted as{created: 0, updated: 0, deleted: 0, unchanged: 1}, which reads as "nothing to do" for an edit the commit applies (cab on GSE49354.1 ยท FV 213751 ยท 2026-08-27). -
testPreviewReportsARenameOfAKeptFactor
@Test public void testPreviewReportsARenameOfAKeptFactor()The factor half of the same gap: renaming a kept factor touches no factor value at all. -
testARoundTripDoesNotMigrateARetiredTermUri
@Test public void testARoundTripDoesNotMigrateARetiredTermUri()๐ A design round-trip must not perform the parked term-URI migration.The API serves canonical URIs, so a client that edits nothing still proposes the canonical form of every retired URI it was shown. Two things went wrong with that: the diff compared the canonical proposal against the raw entity and called it an edit โ cab saw a constant surplus of +1 or +2 `updated` on 8 datasets, none of it theirs โ and committing wrote the canonical value onto the row, performing
scripts/sql/term_uri_migration.sqlone statement at a time on whatever rode along with an unrelated edit.The fixture uses a pair the shim carries: stored
CLO_0007365"LNCAP cell" is served asCLO_0037116"LNCaP cell". -
testPreviewReportsNoUpdatesWhenNothingIsEdited
@Test public void testPreviewReportsNoUpdatesWhenNothingIsEdited()The other half of the rule: a proposal that changes nothing reports nothing. Without this the two counters above could be satisfied by a report that calls every design edited. -
testPreviewDoesNotReportAnEchoOfTheSerializedProjection
@Test public void testPreviewDoesNotReportAnEchoOfTheSerializedProjection()๐ The shape an HTTP client actually round-trips.AbstractFactorValueValueObjectSerializerwrites a statement intostatementsonly when it has an object, so a plaintreatment: controlcomes back to the client undercharacteristicswithstatements: []. Echoing that back is not an edit โ the characteristic claims the same row by id and rewrites the same subject โ and reading the statements list alone made it look like a deletion of every such row on every full-design PUT. -
testPreviewBlocksUnknownFactorId
@Test public void testPreviewBlocksUnknownFactorId() -
testPreviewBlocksUnknownFactorValueId
@Test public void testPreviewBlocksUnknownFactorValueId() -
testPreviewBlocksTypeChangeWhenFactorHasValues
@Test public void testPreviewBlocksTypeChangeWhenFactorHasValues() -
testPreviewBlocksAssignmentReferencingUnknownFv
@Test public void testPreviewBlocksAssignmentReferencingUnknownFv() -
testPreviewBlocksUnknownBiomaterial
@Test public void testPreviewBlocksUnknownBiomaterial() -
testApplyNoChangeIsIdempotentNoOp
@Test public void testApplyNoChangeIsIdempotentNoOp() -
testApplyRejectsBlockerWithIllegalArgumentException
@Test public void testApplyRejectsBlockerWithIllegalArgumentException() -
testApplyBaselineOnlyChangeIsAppliedAndReflected
@Test public void testApplyBaselineOnlyChangeIsAppliedAndReflected()Setting only a factor value's baseline flag is a real change: it must NOT be short-circuited as a no-op, must be written to the entity, and must be reflected in the rebuilt design returned by the apply. Regression guard for the round-trip gap where isNoOpDesignApply ignored in-place kept-FV edits (baseline PUT accepted but the flag never came back). -
testApplyDesignatingBaselineLeavesSiblingAlone
@Test public void testApplyDesignatingBaselineLeavesSiblingAlone()Designating a baseline leaves its siblings alone. Clearing them used to make a second baseline impossible to record โ marking B silently unmarked A โ so a dataset holding two experiments could never carry its two reference levels.nullstill means "no change", so a client that omits the field is unaffected.The one-reference-level requirement is real, but it belongs to ANALYSIS:
LinearModelAnalyzerrefuses a multi-baseline factor unless a subset factor is configured. -
testApplyStatementOnlyEditIsAppliedAndReflected
@Test public void testApplyStatementOnlyEditIsAppliedAndReflected()Editing only a kept factor value's statement (no structural add/delete) is a real change: it must not be short-circuited as a no-op, and the edit must reach the entity. Guards the statement half of the kept-FV no-op gap. -
testMultipleBaselinesInFactorIsAllowed
@Test public void testMultipleBaselinesInFactorIsAllowed()A factor MAY designate more than one baseline: a dataset holding two experiments has a reference level per experiment, and a curator has to be able to record that. Curation does not block it โ the constraint lives where it bites, inLinearModelAnalyzer, which refuses to run such a factor as a single contrast unless a subset factor is configured. -
testCommitCurationRefreshesEe2cAtTheExperimentLevelForATagChange
@Test public void testCommitCurationRefreshesEe2cAtTheExperimentLevelForATagChange()A tag add/remove reaches EE2C at the ExpressionExperiment level and at no other. -
testCommitCurationRefusesATagDeleteThatNamesNoTagOfTheExperiment
@Test public void testCommitCurationRefusesATagDeleteThatNamesNoTagOfTheExperiment()A tag delete that names nothing on the experiment is refused before anything writes, on the commit and on the dry run. It used to be skipped: the commit answered 200 with a lower count while the other sections applied. -
testCommitCurationRefusesASampleCharacteristicDeleteThatNamesNothingOnTheExperiment
@Test public void testCommitCurationRefusesASampleCharacteristicDeleteThatNamesNothingOnTheExperiment()The same refusal for a sample characteristic that belongs to no sample of the experiment. -
testCommitCurationRefreshesEe2cAtTheBioMaterialLevelForASampleCharacteristicChange
@Test public void testCommitCurationRefreshesEe2cAtTheBioMaterialLevelForASampleCharacteristicChange()A per-sample characteristic change reaches EE2C at the BioMaterial level and at no other. -
testCommitCurationRefreshesEe2cAtTheDesignLevelForAStatementReTerm
@Test public void testCommitCurationRefreshesEe2cAtTheDesignLevelForAStatementReTerm()Re-terming a statement on a kept factor value reaches EE2C at the ExperimentalDesign level. -
testCommitCurationDryRunRefreshesNothing
@Test public void testCommitCurationDryRunRefreshesNothing()A preflight computes the same tallies without writing, so it must not rebuild EE2C either โ the rebuild would publish annotations the dry run did not apply. -
testCommitCurationWithNoChangeRefreshesNothing
@Test public void testCommitCurationWithNoChangeRefreshesNothing()A commit whose sections all matched what was already stored pays for no rebuild. -
testApplyEditingAFlattenedStatementKeepsItsSecondObject
@Test public void testApplyEditingAFlattenedStatementKeepsItsSecondObject()The curator edits the subject and nothing else. The second clause is not theirs to lose: it rode in on the second wire entry, which carries it under the generic object keys, and the write has to put it back where it came from. -
testPreviewEchoingTheWireFormIsNotAnEdit
@Test public void testPreviewEchoingTheWireFormIsNotAnEdit()And the echo is a no-op: before the halves were re-joined, one entry became two content keys neither of which matched the stored statement, so an unedited round-trip counted as an edit on every compound statement in the design. -
testPreviewRefusesAThirdEntryForOneStatementId
@Test public void testPreviewRefusesAThirdEntryForOneStatementId()A statement holds two objects and no more, so a third entry for one id is refused rather than dropped. -
testPreviewRefusesAPredicateWithoutAnObject
@Test public void testPreviewRefusesAPredicateWithoutAnObject()Half a first pair is refused on the design routes, which take the value objects directly. -
testPreviewRefusesASecondPredicateWithoutASecondObject
@Test public void testPreviewRefusesASecondPredicateWithoutASecondObject()And half a second pair, which only PUT /curation refused before. -
testPreviewRefusesAFreeTextValueLongerThanTheColumn
@Test public void testPreviewRefusesAFreeTextValueLongerThanTheColumn()A free-text value longer thanFACTOR_VALUE.VALUEis refused by the preflight rather than by the flush. On experiment 38401 the curation UI sent back Gemma's own statement summary as the free-text label โ 318 characters on 7 of that design's 18 factor values โ and the apply wrote it through, so MySQL answeredData too long for column 'VALUE'and the curator got a 500 on a commit the UI had already shown as applied. -
testPreviewAcceptsAFreeTextValueThatExactlyFillsTheColumn
@Test public void testPreviewAcceptsAFreeTextValueThatExactlyFillsTheColumn()One of exactly the length the column holds is not refused -- the guard is off-by-one otherwise. -
testApplyFactorDescriptionOnlyEditIsNotANoOp
@Test public void testApplyFactorDescriptionOnlyEditIsNotANoOp()W11 โ a factor description-only edit is a real change. Nothing structural moves, so every preflight counter stays at zero andhasKeptFactorValueEdits(which only inspects factor values) sees nothing; the apply must still reachupdateFactorMetadatarather than short-circuiting. -
testApplyNullFactorFieldsChangeNothing
@Test public void testApplyNullFactorFieldsChangeNothing()The omission contract (Paul, 2026-09-13): on agemmaIdfactor item, a null field is no change. cab writes a relevance hint on an existing factor with an item carrying nothing else. -
testApplyFactorNameOnlyEditIsNotANoOp
@Test public void testApplyFactorNameOnlyEditIsNotANoOp()W11 โ same gap, reached through the factor name. A rename with no structural change is the other metadata-only edit the curation side sends. -
testApplyFactorCategoryOnlyEditIsNotANoOp
@Test public void testApplyFactorCategoryOnlyEditIsNotANoOp()W11 / W1 โ a factor category re-term with the factor values left in place. This is the edit that changes what the factor means while every id survives, so dropping it silently is the worst of the three: readers keep seeing the old category and nothing signals otherwise. -
testApplyMeasurementEditOnKeptFactorValueIsApplied
@Test public void testApplyMeasurementEditOnKeptFactorValueIsApplied()W14 โ editing the measurement on a kept continuous factor value.applyFactorValueChangesupdates statements, the deprecatedvalue, and the baseline flag on an existing factor value; the measurement is the fourth field a continuous factor actually carries. -
testCreateFactorValueCarriesMeasurementUnit
@Test public void testCreateFactorValueCarriesMeasurementUnit()W14 โ a newly created continuous factor value must keep its unit.createFactorValuecopies the measurement's value, representation and type; the unit is what makes "37" mean anything. -
testApplyAddressesSameCategorySameNameFactorsById
@Test public void testApplyAddressesSameCategorySameNameFactorsById()W9 โ two factors sharing both category and name. The write path must address them by id; any (category, name) keying would edit whichever one it hit first. 20 of the curation side's 500 experiments carry such sibling pairs. -
testApplyAddressesIdenticalLabelFactorValuesById
@Test public void testApplyAddressesIdenticalLabelFactorValuesById()W10 โ two factor values under one factor whose statements serialize identically (distinguished only by a zygosity statement the curation side adds). Addressing one by id must change only that one; any label-keyed match would edit both, or the wrong one, and would do it silently. -
testApplyPreservesUngroundedFreeTextStatement
@Test public void testApplyPreservesUngroundedFreeTextStatement()W8 โ a factor value carrying deliberately ungrounded free text (JAX strain nomenclature, hybrid backgrounds with no ontology term in existence) round-trips with the text intact. The apply path must not coerce, drop, or auto-bind it to a nearest term. -
testApplyPreservesNonAsciiCuratedText
@Test public void testApplyPreservesNonAsciiCuratedText()W16 โ non-ASCII in curated text survives the apply byte-exact. The curation side repaired unicode our GEO import mangled in 68 experiments, so a write path that re-mangles undoes the repair. -
testPreviewDeletingContinuousFactorReportsItsMeasurementValues
@Test public void testPreviewDeletingContinuousFactorReportsItsMeasurementValues()W3 โ deleting a continuous factor takes its measurement-bearing factor values with it, and the preflight says so before the caller commits. -
testApplyWritesSupportingEvidenceOntoAStatement
@Test public void testApplyWritesSupportingEvidenceOntoAStatement()W13 โ supporting evidence attached to a factor-value statement reaches the entity. The design section is the bulk of what curation produces, and until now it was the one section that could not carry a justification at all. -
testApplyEvidenceOnlyEditIsNotANoOp
@Test public void testApplyEvidenceOnlyEditIsNotANoOp()Attaching evidence to an otherwise-unchanged statement leaves every content key identical, so the structural summary andstatementsChangedboth see nothing. Without a dedicated check this is swallowed as a no-op โ the same defect class as the factor description-only edit above. -
testApplyClearsEvidenceWhenThePayloadOmitsIt
@Test public void testApplyClearsEvidenceWhenThePayloadOmitsIt()๐ This service applies FULL-RECORD REPLACEMENT, so an omittedsupportingEvidenceCLEARS a stored one and counts as a change. It does not follow thenull = "no change"convention, and this test asserted that it did untilce61a2f9cb(2026-09-06) retired the hybrid contract.The protection against a client silently dropping provenance somebody else recorded is
DatasetsWebService.requireEvidenceEchoed, which refuses the omission with a 400 before it ever reaches this method โ REST is the only caller. Pinned here so nobody restores "omission is a no-op" in the service and quietly gives the payload two contracts again. -
testAnEchoOfSpacedJsonEvidenceIsNotAnUpdate
@Test public void testAnEchoOfSpacedJsonEvidenceIsNotAnUpdate()frinkbro's notes, rewritten as Python-serialized JSON on 2026-09-13, preflighted as an update on every experiment that carried one (GSE4036, GSE3489): the echo is compact and the comparison was by string. An echo of the same content is not an edit, and the apply keeps the stored bytes. -
testEvidenceThatCannotBeReadSurvivesAProposalWithoutEvidence
@Test public void testEvidenceThatCannotBeReadSurvivesAProposalWithoutEvidence()Stored evidence that is not JSON is served as nothing, so no client can echo it. A proposal without evidence leaves it in place instead of clearing it (Paul approved, 2026-09-13), and the preflight does not report it. -
testApplyWritesEvidenceCodeOntoAStatement
@Test public void testApplyWritesEvidenceCodeOntoAStatement()An evidence code stated on a factor-value statement reaches the entity, so a design write can say how it was decided. -
testApplyEvidenceCodeOnlyEditIsNotANoOp
@Test public void testApplyEvidenceCodeOnlyEditIsNotANoOp()An evidence code is as invisible to the structural summary and tostatementsChangedas supporting evidence is, so a code-only edit needs the same dedicated check or it is swallowed as a no-op. -
testApplyClearsTheEvidenceCodeWhenThePayloadOmitsIt
@Test public void testApplyClearsTheEvidenceCodeWhenThePayloadOmitsIt()The evidence code replaces on the same terms as supporting evidence: an omitted one CLEARS a stored code and counts as a change.9923b7c62dput the refusal one layer up, inDatasetsWebService, after a client echoing a statement without carrying the code back destroyed a stored IC and got an ordinaryupdated: 1.The clear is spelled
""rather than an absent key, becauseevidenceCodeis a String and absent-vs-explicit-null are not distinguishable on one. -
testPreviewSubsetWithLostAnchorRequiresForce
@Test public void testPreviewSubsetWithLostAnchorRequiresForce()A stranded subset requires the same explicit consent as the analysis cascade. It is the more dangerous of the two precisely because it survives the change: still there, still named, still listed, and now anchored on factor values that were deleted out from under it. -
testPreviewWithoutConsequencesDoesNotRequireForce
@Test public void testPreviewWithoutConsequencesDoesNotRequireForce()A change with no analyses to delete and no stranded subsets proceeds without consent. -
testCorrelateNewDesignIdsMapsNewFactorsAndValues
@Test public void testCorrelateNewDesignIdsMapsNewFactorsAndValues() -
testCorrelateNewFactorValueUnderExistingFactor
@Test public void testCorrelateNewFactorValueUnderExistingFactor() -
testBuildAssignmentPassWiresNewFvToBiomaterial
@Test public void testBuildAssignmentPassWiresNewFvToBiomaterial() -
testPreviewSubsetWithLostAnchorIsFlaggedButNotBlocked
@Test public void testPreviewSubsetWithLostAnchorIsFlaggedButNotBlocked()Re-enabled 2026-08-16. It was not a preflight defect: the test stubbedeeDao.getSubSets, whilepreviewDesignChangereads throughsubSetReadService.getSubSetsWithBioAssays, so the unstubbed mock returned an empty list and no subset could ever be flagged. Stubbing the source the code actually reads makes the detection visible. -
testPreviewAddingAWholeFactorDeletesNoAnalysis
@Test public void testPreviewAddingAWholeFactorDeletesNoAnalysis()Adding a factor deletes nothing: the existing analyses still test what they tested. Re-running one to take in the new factor is the curator's call (Paul, 2026-09-13). -
testPreviewStillDeletesAnAnalysisWhoseFactorsCannotBeRead
@Test public void testPreviewStillDeletesAnAnalysisWhoseFactorsCannotBeRead()An analysis whose factors cannot be read is still deleted by a structural change: not knowing what it uses is not evidence that it uses nothing. -
testPreviewFlippingABaselineInvalidatesTheAnalyses
@Test public void testPreviewFlippingABaselineInvalidatesTheAnalyses()A baseline flip moves no structural counter, so it rode through as "safe" and quietly falsified the analysis: the stored result set still names the OLD reference, and every contrast is relative to it. -
testPreviewFlaggingTheBaselineTheAnalysisWasFittedWithDoesNotInvalidateIt
@Test public void testPreviewFlaggingTheBaselineTheAnalysisWasFittedWithDoesNotInvalidateIt()๐ Flagging the value the analysis was already fitted against moves no reference. GSE391: result set 559530 names FV 783 as baseline while its flag is stored null, and a draft settingisBaseline: trueon it was refused as deleting DEA 316735 (Paul, 2026-09-12). -
testPreviewFlaggingADifferentBaselineThanTheFittedOneStillInvalidates
@Test public void testPreviewFlaggingADifferentBaselineThanTheFittedOneStillInvalidates()The other side of that exemption: flagging a value other than the fitted reference still invalidates. -
testPreviewRelabellingAKeptFactorValueDoesNotInvalidateAnything
@Test public void testPreviewRelabellingAKeptFactorValueDoesNotInvalidateAnything()The exclusion, and the only one: relabelling a kept factor value does not move a sample, a level or a reference, so the analysis still describes what it described. -
testApplyRemovesTheAnalysesThePreflightEnumerated
@Test public void testApplyRemovesTheAnalysesThePreflightEnumerated()The apply side executes the report's list instead of deriving its own. It used to derive its own, and the two derivations disagreed: the preflight warned that a baseline flip would delete an analysis, the curator consented withforce=true, and the apply deleted nothing โ leaving an analysis whose every contrast points at the old reference. -
testApplyRemovesNoAnalysisForALabelOnlyEdit
@Test public void testApplyRemovesNoAnalysisForALabelOnlyEdit()The other half of the same contract: the one excluded edit removes nothing. A relabel that deleted analyses would make every wording fix cost a re-run. -
testPreviewKeepsAnAnalysisOnAFactorTheChangeDoesNotReach
@Test public void testPreviewKeepsAnAnalysisOnAFactorTheChangeDoesNotReach()An analysis on another factor survives a change to the treatment factor. Before 2026-09-13 it went too. -
testPreviewReplacingAFactorNoAnalysisUsesDeletesNoAnalysis
@Test public void testPreviewReplacingAFactorNoAnalysisUsesDeletesNoAnalysis()The re-type frinkbro preflighted on GSE93069 (2026-09-12): delete a categorical age factor and create a continuous one. The only analysis tests another factor, so nothing is deleted, although a sample loses its age value. -
testDroppedFactorIsStillInItsDesignWhenTheSecuredRemoveIsCalled
@Test public void testDroppedFactorIsStillInItsDesignWhenTheSecuredRemoveIsCalled()๐ A dropped factor is STILL IN its design's factor collection whenexperimentalFactorService.removeis called. Authorization depends on it, so detaching first denies the call.ExperimentalFactorService#removeis@Secured({"GROUP_USER","ACL_SECURABLE_EDIT"}), and an ExperimentalFactor is a SecuredChild whose ACL parentParentIdentityRetrievalStrategyImplresolves viaExpressionExperimentDao.findIdByFactorโ HQL that joinsed.experimentalFactors. Removing the factor from that collection before the call makes the query auto-flush the pending change and match no row: parent identity null, the factor's ACL cannot inherit from the experiment, and the vote denies with "Access is denied" even for an administrator, because it is a lookup that found nothing rather than a permission that was refused.๐ This class mocks the services, so there is no security proxy here and no 403 to assert. What it can pin is the ordering the proxy depends on โ the same reason a missing
@Transactionalpasses a mocked test and 500s live. The detach itself is not skipped:ExperimentalFactorServiceImpl.removeperforms it at its own line 67, after the interceptor has passed.cab hit this on GSE19804 (2026-09-09), signing off a categorical โ continuous age re-type. The 403 landed after
deleteAnalysishad already removed DEA 432031's archive, so the rollback left a surviving analysis without its cached files.
-