mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
31fcc4f4d6
Stacked on #2725 (Split a manually linked pair when the API unlinks it). Until that merges this PR carries its commit too; only the last commit is new here. ## Problem `POST /api/resources/{id}/report-merge` (the drawer's "Split merged resource" dialog) excluded only the candidate IDs that `ResourceRegistry.SourceTargets` derives with `sourceSpecificID(mergedResource.Type, source, sourceID)`. For a source an operator link folded in, that candidate names neither side of the link: - an agent linked into a VM gets a `vm-` candidate; - an agent linked with a node or a Docker host gets a source-specific candidate, while the folded side's canonical ID derives from its machine ID. No `resource_links` row matched, so even with #2725's one-decision-per-pair store rule the link survived, the pair stayed merged on REST, `/api/state` and the websocket, and the API answered 200 "Merge reported" while the dialog toasted "Resource split applied". Two linked agents share their only source, so the API refused them as "Resource is not merged". Identity merges were already handled: `findMatch` checks exactly the candidate `SourceTargets` reports, and an agent/Docker pair merged by machine identity stays apart through three monitor rebuilds after report-merge. That is now pinned by a test. ## Fix - `applyManualLinks` records each link it applies on the merged resource (`recordManualLinkFold`, unexported `Resource.linkFolds`): the link row's own pair plus the sources the folded side brought, carrying the folded side's earlier folds so each link along a chain keeps its pair. - The record rides in-memory clones. The resources API seeds its registry from the monitor's already-linked listing and never holds the folded row, so this is the only place it can learn the pair. - `HandleReportMerge` excludes the pair of every link whose folded side brought a reported source (`ResourceRegistry.ManualLinkFolds`), alongside the existing identity-match exclusions, deduplicated in either order. #2725's `AddExclusion` then deletes the link row, so the pair splits from the monitor's next rebuild, as with unlink. An applied link also counts as a merge, so two linked agents can be reported. - A pair is recorded once. Record ingest can recreate a folded side whose source mapping points at a holder of another type, then fold it again. - A link joins only the pair its row names. Canonical-ID succession re-keys link endpoints with `UPDATE OR IGNORE` but moves `primary_id` unconditionally. A row whose re-key collided with the successor's own row therefore kept the retired endpoint and took the successor as its primary, and `applyManualLinks` merged the successor through it. No unlink or report-merge of the merged pair removed that row. A primary naming neither side now falls back to the row's first side, which leaves such a row inert. The drawer sends every merged source. Source categories are not attributed to individual contributors, so naming one that a folded side carried undoes that link. A source that arrived along a chain undoes every link on its way to the merged resource. ## Verification - `TestResourceReportMergeSplitsOperatorLinkedPair`: link and report-merge through the API handlers against a `MonitorAdapter` on the same SQLite store, rebuilt three times per step. Cases cover VM/agent, node/agent and agent/Docker pairs linked from either side, plus two linked agents. With #2725's handler, 6 of the 7 cases fail: five with "report-merge left 1 monitor resources", and the agent pair with a 400. The exception is a node linked from its agent, because the node's source-specific candidate happens to be its own ID. - `TestResourceReportMergeSplitsPairBehindASuccessionShadowedRow`, `TestResourceReportMergeSourceFilterSelectsLinks`, `TestResourceReportMergeSplitsIdentityMergedPair`, `TestContract_ResourceReportMergeReplacesOperatorLink` (response shape plus link removal), `TestManualLinkFoldsNameEachLinkPairAlongAChain` (monitor registry and a registry seeded from its listing), `TestManualLinkFoldsRecordEachPairOnceAcrossRecordIngests`, `TestManualLinkFoldRepeatedPairKeepsEarlierSources`, `TestCloneResource_CarriesLinkFolds`, and the `TestResourceAPIReportMergeExcludesRegistryLinkFolds` guardrail. - Negative controls, each failing at the expected assertion: the handler from #2725, the old not-merged gate, the primary honoured outside its row, folds appended without dedupe, a repeated pair keeping only its later sources, `applyManualLinks` without `recordManualLinkFold`, and a clone that shares the fold slice. - Full `./internal/unifiedresources/...`, `./internal/api/resourceapi/...`, `./internal/truenas/...` and `./internal/monitoring/...` pass.