fix: correct false breakage detection for flavored packages and confi… (#3149)

fix: correct false breakage detection for flavored packages and config changes

Two bugs caused the UI to incorrectly warn about dependency breakages:

1. dryUpdate (version path): Flavored package versions (e.g. #knots:27.0.0:0)
   failed exver.satisfies() against flavorless ranges (e.g. >=26.0.0) due to
   flavor mismatch. Now checks the manifest's `satisfies` declarations,
   matching the pattern already used in DepErrorService. Added `satisfies`
   field to PackageVersionInfo so it's available from registry data.

2. checkConflicts (config path): fast-json-patch's compare() treated missing
   keys as conflicts (add ops) and used positional array comparison, diverging
   from the backend's conflicts() semantics. Replaced with a conflicts()
   function that mirrors core/src/service/action.rs — missing keys are not
   conflicts, and arrays use set-based comparison.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Hill
2026-03-29 13:07:52 -06:00
committed by GitHub
parent b0b4b41c42
commit e3b7277ccd
11 changed files with 69 additions and 13 deletions

View File

@@ -31,7 +31,7 @@ import { hasCurrentDeps } from 'src/app/utils/has-deps'
import { MarketplaceAlertsService } from '../services/alerts.service'
type KEYS = 'id' | 'version' | 'alerts' | 'flavor'
type KEYS = 'id' | 'version' | 'alerts' | 'flavor' | 'satisfies'
@Component({
selector: 'marketplace-controls',
@@ -185,9 +185,13 @@ export class MarketplaceControlsComponent {
}
private async dryInstall(url: string | null) {
const { id, version } = this.pkg()
const { id, version, satisfies } = this.pkg()
const packages = await getAllPackages(this.patch)
const breakages = dryUpdate({ id, version }, packages, this.exver)
const breakages = dryUpdate(
{ id, version, satisfies: satisfies || [] },
packages,
this.exver,
)
if (!breakages.length || (await this.alerts.alertBreakages(breakages))) {
this.installOrUpload(url)

View File

@@ -14,7 +14,6 @@ import {
TuiNotification,
} from '@taiga-ui/core'
import { injectContext } from '@taiga-ui/polymorpheus'
import * as json from 'fast-json-patch'
import { compare } from 'fast-json-patch'
import { PatchDB } from 'patch-db-client'
import { catchError, EMPTY, endWith, firstValueFrom, from, map } from 'rxjs'
@@ -191,9 +190,7 @@ export class ActionInputModal {
task.actionId === this.actionId &&
task.when?.condition === 'input-not-matches' &&
task.input &&
json
.compare(input, task.input.value)
.some(op => op.op === 'add' || op.op === 'replace'),
conflicts(task.input.value, input),
),
)
.map(id => id)
@@ -214,3 +211,26 @@ export class ActionInputModal {
)
}
}
// Mirrors the Rust backend's `conflicts()` function in core/src/service/action.rs.
// A key in the partial that is missing from the full input is NOT a conflict.
function conflicts(left: unknown, right: unknown): boolean {
if (
typeof left === 'object' &&
left !== null &&
!Array.isArray(left) &&
typeof right === 'object' &&
right !== null &&
!Array.isArray(right)
) {
const l = left as Record<string, unknown>
const r = right as Record<string, unknown>
return Object.keys(l).some(k => (k in r ? conflicts(l[k], r[k]) : false))
}
if (Array.isArray(left) && Array.isArray(right)) {
return left.some(v => right.every(vr => conflicts(v, vr)))
}
return left !== right
}

View File

@@ -459,6 +459,7 @@ export namespace Mock {
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
satisfies: [],
dependencyMetadata: {},
donationUrl: null,
alerts: {
@@ -501,6 +502,7 @@ export namespace Mock {
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
satisfies: [],
dependencyMetadata: {},
donationUrl: null,
alerts: {
@@ -553,6 +555,7 @@ export namespace Mock {
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
satisfies: [],
dependencyMetadata: {},
donationUrl: null,
alerts: {
@@ -595,6 +598,7 @@ export namespace Mock {
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
satisfies: [],
dependencyMetadata: {},
donationUrl: null,
alerts: {
@@ -649,6 +653,7 @@ export namespace Mock {
gitHash: 'fakehash',
icon: LND_ICON,
sourceVersion: null,
satisfies: [],
dependencyMetadata: {
bitcoind: BitcoinDep,
'btc-rpc-proxy': ProxyDep,
@@ -704,6 +709,7 @@ export namespace Mock {
gitHash: 'fakehash',
icon: LND_ICON,
sourceVersion: null,
satisfies: [],
dependencyMetadata: {
bitcoind: BitcoinDep,
'btc-rpc-proxy': ProxyDep,
@@ -763,6 +769,7 @@ export namespace Mock {
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
satisfies: [],
dependencyMetadata: {},
donationUrl: null,
alerts: {
@@ -805,6 +812,7 @@ export namespace Mock {
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
satisfies: [],
dependencyMetadata: {},
donationUrl: null,
alerts: {
@@ -857,6 +865,7 @@ export namespace Mock {
gitHash: 'fakehash',
icon: LND_ICON,
sourceVersion: null,
satisfies: [],
dependencyMetadata: {
bitcoind: BitcoinDep,
'btc-rpc-proxy': ProxyDep,
@@ -912,6 +921,7 @@ export namespace Mock {
gitHash: 'fakehash',
icon: PROXY_ICON,
sourceVersion: null,
satisfies: [],
dependencyMetadata: {
bitcoind: BitcoinDep,
},

View File

@@ -3,7 +3,11 @@ import { DataModel } from '../services/patch-db/data-model'
import { getManifest } from './get-package-data'
export function dryUpdate(
{ id, version }: { id: string; version: string },
{
id,
version,
satisfies,
}: { id: string; version: string; satisfies: string[] },
pkgs: DataModel['packageData'],
exver: Exver,
): string[] {
@@ -13,10 +17,24 @@ export function dryUpdate(
Object.keys(pkg.currentDependencies || {}).some(
pkgId => pkgId === id,
) &&
!exver.satisfies(
!versionSatisfies(
version,
satisfies,
pkg.currentDependencies[id]?.versionRange || '',
exver,
),
)
.map(pkg => getManifest(pkg).title)
}
function versionSatisfies(
version: string,
satisfies: string[],
range: string,
exver: Exver,
): boolean {
return (
exver.satisfies(version, range) ||
satisfies.some(v => exver.satisfies(v, range))
)
}