Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export interface AxisPresetBrowserEntrySummary {
folder: string | null;
tags: string[];
blocks: AxisPresetBrowserBlockSummary[];
/** Decoded per-family model names (e.g. { amp: ["5153 100W Blue"] }) — the source of truth for
* TYPE-style query matching; `blocks[].name` is only a generic roster instance label. */
models: Record<string, string[]>;
amps: string[];
/** Resolved cloud sync state (from cloud.stateOf via the host); 'none' when signed out. */
syncState: SyncState;
/** A synthesized cloud-only row (host id starts with `cloud:`). */
Expand Down Expand Up @@ -235,6 +239,8 @@ function normalizeEntry(
folder: entry.folder ?? null,
tags: tagsOf?.(entry.id) ?? [],
blocks,
models: entry.summary.models ?? {},
amps: entry.summary.amps ?? [],
syncState: syncStateOf?.(entry) ?? 'none',
cloudOnly: entry.id.startsWith('cloud:'),
converted,
Expand Down
Binary file modified src/lib/axis-workbench/presetBrowser/presetBrowserWorkbenchQuery.ts
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,10 @@ export function axisPbRowBlockChips(entry: AxisPresetBrowserEntrySummary): AxisP
const slug = (block.slug ?? '').toLowerCase();
if (!slug || IO_SLUGS.has(slug)) continue;
const cat = axisPbCatLabel(slug);
// The summary block "name" is the model/type name for that slot when decoded (e.g. "USA Clean");
// when it just echoes the category we drop it so the chip stays "Cat".
const rawType = (block.name ?? '').trim();
// The decoded model name for this family (e.g. "USA Clean") is preferred, mirroring the monolith's
// blocksOf typeName; `block.name` is only a generic roster instance label ("Amp 1") and is the
// fallback when nothing was decoded. When it just echoes the category we drop it so the chip stays "Cat".
const rawType = ((entry.models[slug] ?? [])[0] ?? block.name ?? '').trim();
const type = rawType && rawType.toLowerCase() !== cat.toLowerCase() ? rawType : null;
const instance = block.instance != null ? `${cat} ${block.instance}` : cat;
chips.push({
Expand Down
41 changes: 41 additions & 0 deletions src/lib/axis-workbench/test/presetBrowserWorkbenchData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,45 @@ describe('Preset Browser Workbench data view', () => {
number: null
});
});

it('carries decoded amp/block model names through to AMP(TYPE=...) query matching (regression: the mirror used to drop them)', () => {
const fm3: AxisPresetBrowserLibEntryLike = {
id: 'dev:fm3-1',
source: 'device',
summary: {
number: 2,
name: '5153 Lead',
model: 'FM3',
scenes: [],
blocks: [{ effectId: 101, slug: 'amp', name: 'Amp 1', instance: 1 }],
models: { amp: ['5153 100W Blue'] },
amps: ['5153 100W Blue']
}
};
const view = (conditions: Parameters<typeof createAxisPresetBrowserDataView>[0]['conditions']) =>
createAxisPresetBrowserDataView({ entries: [fm3], conditions }).visibleEntries.map((e) => e.id);

expect(view([{ kind: 'block', block: 'amp', params: [{ name: 'TYPE', op: '=', val: '5153' }] }])).toEqual([
'dev:fm3-1'
]);
expect(view([{ kind: 'block', block: 'amp', params: [{ name: 'TYPE', op: '=', val: 'marshall' }] }])).toEqual([]);
expect(view([{ kind: 'block', block: 'amp', params: [{ name: 'TYPE', op: '!=', val: 'marshall' }] }])).toEqual([
'dev:fm3-1'
]);
expect(view([{ kind: 'block', block: 'amp', params: [{ name: 'TYPE', op: '!=', val: '5153' }] }])).toEqual([]);
});

it('does not throw on cloud-only entries with empty models/amps maps', () => {
const cloudOnly: AxisPresetBrowserLibEntryLike = {
id: 'cloud:9',
source: 'device',
summary: { number: 9, name: 'Cloud Only', model: 'FM3', scenes: [], blocks: [], models: {}, amps: [] }
};
expect(() =>
createAxisPresetBrowserDataView({
entries: [cloudOnly],
conditions: [{ kind: 'block', block: 'amp', params: [{ name: 'TYPE', op: '=', val: '5153' }] }]
})
).not.toThrow();
});
});
47 changes: 47 additions & 0 deletions src/lib/axis-workbench/test/presetBrowserWorkbenchQuery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
import {
condsEqual,
condsToQuery,
matchEntryFromSummary,
matchNumeric,
matchPreset,
parseQuery,
Expand All @@ -11,6 +12,7 @@ import {
toSimpleConds,
type AxisPbMatchEntry
} from '../presetBrowser/presetBrowserWorkbenchQuery';
import type { AxisPresetBrowserEntrySummary } from '../presetBrowser/presetBrowserWorkbenchData';

const entry = (over: Partial<AxisPbMatchEntry> = {}): AxisPbMatchEntry => ({
name: 'Studio Clean',
Expand Down Expand Up @@ -103,3 +105,48 @@ describe('Preset Browser matching', () => {
expect(matchPreset(entry(), parseQuery('tag:Live + COMP'), 'studio')).toBe(false);
});
});

describe('matchEntryFromSummary (regression: decoded models must survive summary normalization)', () => {
const summaryEntry = (over: Partial<AxisPresetBrowserEntrySummary> = {}): AxisPresetBrowserEntrySummary => ({
id: 'dev:1',
sourceId: 'device',
sourceLabel: 'Device',
number: 2,
name: '5153 Lead',
model: 'FM3', // the device model string — must never leak into the amp model list
sceneCount: 0,
blockCount: 1,
fav: false,
folder: null,
tags: [],
blocks: [{ effectId: 101, slug: 'amp', name: 'Amp 1', instance: 1 }],
models: { amp: ['5153 100W Blue'] },
amps: ['5153 100W Blue'],
syncState: 'none',
cloudOnly: false,
converted: false,
provenance: null,
...over
});

it('matches TYPE against the decoded model name, not the generic block label or device string', () => {
const matched = matchEntryFromSummary(summaryEntry());
expect(matchPreset(matched, parseQuery('AMP(TYPE=5153)'), '')).toBe(true);
expect(matchPreset(matched, parseQuery('AMP(TYPE=marshall)'), '')).toBe(false);
expect(matchPreset(matched, parseQuery('AMP(TYPE!=marshall)'), '')).toBe(true);
expect(matchPreset(matched, parseQuery('AMP(TYPE!=5153)'), '')).toBe(false);
expect(matchPreset(matched, [], '5153')).toBe(true);
// the device model string ("FM3") must not be searchable as if it were an amp type.
expect(matchPreset(matched, parseQuery('AMP(TYPE=FM3)'), '')).toBe(false);
});

it('falls back to the generic block label when no decoded model exists for that slug', () => {
const matched = matchEntryFromSummary(summaryEntry({ models: {}, amps: [] }));
expect(matchPreset(matched, parseQuery('AMP(TYPE=Amp 1)'), '')).toBe(true);
});

it('does not throw on an entry with empty models/amps maps (cloud-only shape)', () => {
const matched = matchEntryFromSummary(summaryEntry({ blocks: [], models: {}, amps: [] }));
expect(matchPreset(matched, parseQuery('AMP(TYPE=5153)'), '')).toBe(false);
});
});
12 changes: 12 additions & 0 deletions src/lib/axis-workbench/test/presetBrowserWorkbenchRowChips.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ function entry(over: Partial<AxisPresetBrowserEntrySummary> = {}): AxisPresetBro
folder: null,
tags: [],
blocks: [],
models: {},
amps: [],
syncState: 'none',
cloudOnly: false,
converted: false,
Expand Down Expand Up @@ -53,6 +55,16 @@ describe('row block chips (§4.3)', () => {
expect(chips[1].label).toBe('Reverb');
});

it('prefers the decoded model name over the generic block label (regression)', () => {
const chips = axisPbRowBlockChips(
entry({
blocks: [{ effectId: 2, slug: 'amp', name: 'Amp 1', instance: 1 }],
models: { amp: ['5153 100W Blue'] }
})
);
expect(chips[0].label).toBe('Amp · 5153 100W Blue');
});

it('carries a title of "instance — TYPE"', () => {
const chips = axisPbRowBlockChips(
entry({ blocks: [{ effectId: 2, slug: 'drive', name: 'TS808 Mod', instance: 2 }] })
Expand Down