From 23d66dedb6289165bd311ef28c8177f2f743ed94 Mon Sep 17 00:00:00 2001 From: Jumpstile <29765787+Jumpstile@users.noreply.github.com> Date: Wed, 30 Sep 2026 09:16:39 -0400 Subject: [PATCH 1/3] feat(one-guide): expand category taxonomy for issue 191 --- docs/architecture/ARCHITECTURE.md | 10 ++-- docs/developer/DEVELOPER_GUIDE.md | 15 ++++-- docs/user/CURRENT_LIMITATIONS.md | 11 +++-- schemas/one-guide-projection.schema.json | 38 ++++++++++++-- ...t-ChannelForgeOneGuideCategoryRegistry.ps1 | 17 ++++++- .../Public/Get-ChannelForgeOneGuide.ps1 | 23 ++++++++- tests/unit/OneGuide.Tests.ps1 | 49 ++++++++++++++----- tests/unit/WebServer.Tests.ps1 | 10 +++- 8 files changed, 141 insertions(+), 32 deletions(-) diff --git a/docs/architecture/ARCHITECTURE.md b/docs/architecture/ARCHITECTURE.md index 0738051..d8e4363 100644 --- a/docs/architecture/ARCHITECTURE.md +++ b/docs/architecture/ARCHITECTURE.md @@ -106,9 +106,13 @@ generated, these routes return HTTP 503 with `one-guide-unavailable`. An accepted guide that has no programmes matching a query remains a successful HTTP 200 response with an empty result; unavailable source state is never represented as a valid empty guide. -The Slice 1 built-in category registry contains only Live Now, Starting Soon, -Wrestling, Football, Baseball, Soccer, Movies, and News. It maps only those -recognized XMLTV categories to stable keys; unrecognized categories are omitted. +The built-in category registry is centralized and data-driven. It defines +`live-now`, `starting-soon`, football, baseball, basketball, hockey, soccer, +wrestling, motorsports, boxing, MMA, tennis, golf, rugby, cricket, lacrosse, +`other-sports`, movies, news, kids, entertainment, documentary, and comedy. +Deterministic aliases map recognized XMLTV labels; generic Sports maps to +`other-sports` only when no specific sport category is also present. Unknown +categories remain omitted. Cross-channel grouping requires an explicit `CanonicalEventId` in projection input; the generated item identity also includes the normalized title, subtitle, episode number, and schedule. Without that explicit ID, rows are scoped to a diff --git a/docs/developer/DEVELOPER_GUIDE.md b/docs/developer/DEVELOPER_GUIDE.md index 48b35cd..f611a3e 100644 --- a/docs/developer/DEVELOPER_GUIDE.md +++ b/docs/developer/DEVELOPER_GUIDE.md @@ -553,11 +553,16 @@ The read endpoints are: | `GET`, `HEAD` | `/api/one-guide/category/{key}` | Bounded items in a fixed One Guide category | | `GET`, `HEAD` | `/api/one-guide/items/{item-id}` | One deterministic item detail | -Slice 1 built-in category keys: `live-now`, `starting-soon`, `wrestling`, -`football`, `baseball`, `soccer`, `movies`, and `news`. Category aliases map -deterministically; other XMLTV categories are omitted. Responses use UTC timestamps and contain no raw -URLs, credentials, private paths, accepted hashes, or parser diagnostics. The -server remains loopback-only; authentication and LAN access are not implemented. +Built-in category keys: `live-now`, `starting-soon`, `football`, `baseball`, +`basketball`, `hockey`, `soccer`, `wrestling`, `motorsports`, `boxing`, `mma`, +`tennis`, `golf`, `rugby`, `cricket`, `lacrosse`, `other-sports`, `movies`, +`news`, `kids`, `entertainment`, `documentary`, and `comedy`. The `motorsports` +display label is “Auto Racing & Motorsports.” Category aliases map deterministically. +Generic Sports maps to `other-sports` only when no specific recognized sport +category is present; unknown XMLTV categories are omitted. Responses use UTC +timestamps and contain no raw URLs, credentials, private paths, accepted +hashes, or parser diagnostics. The local server remains loopback-only; +authentication and LAN access are not implemented. The dashboard remains presentation-only and currently consumes `/api/status`; it does not yet render the One Guide routes. Guided Setup and refresh requests diff --git a/docs/user/CURRENT_LIMITATIONS.md b/docs/user/CURRENT_LIMITATIONS.md index 3c46622..a0e062e 100644 --- a/docs/user/CURRENT_LIMITATIONS.md +++ b/docs/user/CURRENT_LIMITATIONS.md @@ -46,10 +46,13 @@ request reports HTTP 503 `one-guide-unavailable`. This is different from a valid accepted guide with no programmes matching the requested query, which returns HTTP 200 with an empty result. Starting Soon means a programme starts after now and within two hours; Live Now -includes its start but excludes its stop. The built-in category keys are -`live-now`, `starting-soon`, `wrestling`, `football`, `baseball`, `soccer`, -`movies`, and `news`. Category aliases are deterministic; other XMLTV -categories are omitted. +includes its start but excludes its stop. Built-in category keys are `live-now`, +`starting-soon`, `football`, `baseball`, `basketball`, `hockey`, `soccer`, +`wrestling`, `motorsports`, `boxing`, `mma`, `tennis`, `golf`, `rugby`, +`cricket`, `lacrosse`, `other-sports`, `movies`, `news`, `kids`, +`entertainment`, `documentary`, and `comedy`. Category aliases are +deterministic; Generic Sports maps to `other-sports` only when no specific +sport category is present. Unknown XMLTV categories are omitted. List `GET` and `HEAD` requests support `limit` (1–100) and `offset` (0–2,147,483,647); item Details does not accept an offset. Malformed or out-of-range values fail with HTTP 400, while an offset beyond the list result diff --git a/schemas/one-guide-projection.schema.json b/schemas/one-guide-projection.schema.json index 58ce3be..50e8c56 100644 --- a/schemas/one-guide-projection.schema.json +++ b/schemas/one-guide-projection.schema.json @@ -23,12 +23,27 @@ "enum": [ "live-now", "starting-soon", - "wrestling", "football", "baseball", + "basketball", + "hockey", "soccer", + "wrestling", + "motorsports", + "boxing", + "mma", + "tennis", + "golf", + "rugby", + "cricket", + "lacrosse", + "other-sports", "movies", - "news" + "news", + "kids", + "entertainment", + "documentary", + "comedy" ] }, "Offset": { "type": "integer", "minimum": 0 }, @@ -46,12 +61,27 @@ "enum": [ "live-now", "starting-soon", - "wrestling", "football", "baseball", + "basketball", + "hockey", "soccer", + "wrestling", + "motorsports", + "boxing", + "mma", + "tennis", + "golf", + "rugby", + "cricket", + "lacrosse", + "other-sports", "movies", - "news" + "news", + "kids", + "entertainment", + "documentary", + "comedy" ] }, "item": { diff --git a/src/ChannelForge/Private/Get-ChannelForgeOneGuideCategoryRegistry.ps1 b/src/ChannelForge/Private/Get-ChannelForgeOneGuideCategoryRegistry.ps1 index 9fb18fb..c210e35 100644 --- a/src/ChannelForge/Private/Get-ChannelForgeOneGuideCategoryRegistry.ps1 +++ b/src/ChannelForge/Private/Get-ChannelForgeOneGuideCategoryRegistry.ps1 @@ -5,11 +5,26 @@ function Get-ChannelForgeOneGuideCategoryRegistry { return @( [pscustomobject][ordered]@{ Key = 'live-now'; DisplayName = 'Live Now'; Group = 'temporal'; Aliases = @() } [pscustomobject][ordered]@{ Key = 'starting-soon'; DisplayName = 'Starting Soon'; Group = 'temporal'; Aliases = @() } - [pscustomobject][ordered]@{ Key = 'wrestling'; DisplayName = 'Wrestling'; Group = 'sports'; Aliases = @('wrestling', 'professional wrestling', 'pro wrestling') } [pscustomobject][ordered]@{ Key = 'football'; DisplayName = 'Football'; Group = 'sports'; Aliases = @('football') } [pscustomobject][ordered]@{ Key = 'baseball'; DisplayName = 'Baseball'; Group = 'sports'; Aliases = @('baseball') } + [pscustomobject][ordered]@{ Key = 'basketball'; DisplayName = 'Basketball'; Group = 'sports'; Aliases = @('basketball') } + [pscustomobject][ordered]@{ Key = 'hockey'; DisplayName = 'Hockey'; Group = 'sports'; Aliases = @('hockey', 'ice hockey') } [pscustomobject][ordered]@{ Key = 'soccer'; DisplayName = 'Soccer'; Group = 'sports'; Aliases = @('soccer', 'association football') } + [pscustomobject][ordered]@{ Key = 'wrestling'; DisplayName = 'Wrestling'; Group = 'sports'; Aliases = @('wrestling', 'professional wrestling', 'pro wrestling') } + [pscustomobject][ordered]@{ Key = 'motorsports'; DisplayName = 'Auto Racing & Motorsports'; Group = 'sports'; Aliases = @('auto racing', 'motor racing', 'motorsports', 'motorcycle racing') } + [pscustomobject][ordered]@{ Key = 'boxing'; DisplayName = 'Boxing'; Group = 'sports'; Aliases = @('boxing') } + [pscustomobject][ordered]@{ Key = 'mma'; DisplayName = 'MMA'; Group = 'sports'; Aliases = @('mma', 'mixed martial arts') } + [pscustomobject][ordered]@{ Key = 'tennis'; DisplayName = 'Tennis'; Group = 'sports'; Aliases = @('tennis') } + [pscustomobject][ordered]@{ Key = 'golf'; DisplayName = 'Golf'; Group = 'sports'; Aliases = @('golf') } + [pscustomobject][ordered]@{ Key = 'rugby'; DisplayName = 'Rugby'; Group = 'sports'; Aliases = @('rugby') } + [pscustomobject][ordered]@{ Key = 'cricket'; DisplayName = 'Cricket'; Group = 'sports'; Aliases = @('cricket') } + [pscustomobject][ordered]@{ Key = 'lacrosse'; DisplayName = 'Lacrosse'; Group = 'sports'; Aliases = @('lacrosse') } + [pscustomobject][ordered]@{ Key = 'other-sports'; DisplayName = 'Other Sports'; Group = 'sports'; Aliases = @('sports', 'sport', 'other sports') } [pscustomobject][ordered]@{ Key = 'movies'; DisplayName = 'Movies'; Group = 'content'; Aliases = @('movie', 'movies', 'film', 'films') } [pscustomobject][ordered]@{ Key = 'news'; DisplayName = 'News'; Group = 'content'; Aliases = @('news', 'current affairs') } + [pscustomobject][ordered]@{ Key = 'kids'; DisplayName = 'Kids'; Group = 'content'; Aliases = @('children', "children's", 'kids') } + [pscustomobject][ordered]@{ Key = 'entertainment'; DisplayName = 'Entertainment'; Group = 'content'; Aliases = @('entertainment') } + [pscustomobject][ordered]@{ Key = 'documentary'; DisplayName = 'Documentary'; Group = 'content'; Aliases = @('documentary', 'documentaries') } + [pscustomobject][ordered]@{ Key = 'comedy'; DisplayName = 'Comedy'; Group = 'content'; Aliases = @('comedy') } ) } diff --git a/src/ChannelForge/Public/Get-ChannelForgeOneGuide.ps1 b/src/ChannelForge/Public/Get-ChannelForgeOneGuide.ps1 index 9bdb064..ecfd22e 100644 --- a/src/ChannelForge/Public/Get-ChannelForgeOneGuide.ps1 +++ b/src/ChannelForge/Public/Get-ChannelForgeOneGuide.ps1 @@ -30,10 +30,14 @@ function Get-ChannelForgeOneGuide { $sourceReferences = [System.Collections.Generic.Dictionary[string, string]]::new([System.StringComparer]::Ordinal) $channelReferences = [System.Collections.Generic.Dictionary[string, string]]::new([System.StringComparer]::Ordinal) $taxonomy = @{} + $specificSportKeys = @{} foreach ($categoryDefinition in $categoryRegistry) { foreach ($alias in @($categoryDefinition.Aliases)) { $taxonomy[[string]$alias.ToLowerInvariant()] = [string]$categoryDefinition.Key } + if ($categoryDefinition.Group -eq 'sports' -and $categoryDefinition.Key -ne 'other-sports') { + $specificSportKeys[[string]$categoryDefinition.Key] = $true + } } function Get-InputValue { @@ -89,12 +93,22 @@ function Get-ChannelForgeOneGuide { $episodeNumber = Get-SafeOptionalText (Get-InputValue $programme 'EpisodeNumber') 128 $categories = [System.Collections.Generic.SortedSet[string]]::new([System.StringComparer]::Ordinal) $sourceCategories = [System.Collections.Generic.List[string]]::new() + $hasGenericSports = $false + $hasSpecificSport = $false foreach ($rawCategory in @(Get-InputValue $programme 'Categories')) { if ($null -eq $rawCategory) { continue } $categoryText = ConvertTo-ChannelForgeGuideSafeText -Value $rawCategory -MaximumLength 128 [void]$sourceCategories.Add([string]$categoryText) $key = $categoryText.Trim().ToLowerInvariant() - if ($taxonomy.ContainsKey($key)) { [void]$categories.Add([string]$taxonomy[$key]) } + if ($key -in @('sport', 'sports')) { $hasGenericSports = $true; continue } + if ($taxonomy.ContainsKey($key)) { + $mappedCategoryKey = [string]$taxonomy[$key] + [void]$categories.Add($mappedCategoryKey) + if ($specificSportKeys.ContainsKey($mappedCategoryKey)) { $hasSpecificSport = $true } + } + } + if ($hasGenericSports -and -not $hasSpecificSport) { + [void]$categories.Add('other-sports') } $sourceCategories.Sort([System.StringComparer]::Ordinal) $sourceCategoryEvidence = $sourceCategories.ToArray() @@ -205,6 +219,8 @@ function Get-ChannelForgeOneGuide { StartUtcTicks = $start.UtcTicks StopUtcTicks = $stop.UtcTicks Status = $status + HasGenericSports = $hasGenericSports + HasSpecificSport = $hasSpecificSport CategoryKeys = [System.Collections.Generic.SortedSet[string]]::new([System.StringComparer]::Ordinal) Sport = $sport League = $league @@ -225,6 +241,8 @@ function Get-ChannelForgeOneGuide { $group.KindConflict = $true } } + if ($hasGenericSports) { $group.HasGenericSports = $true } + if ($hasSpecificSport) { $group.HasSpecificSport = $true } if (-not $group.Offerings.ContainsKey($offeringId)) { $group.Offerings[$offeringId] = $offering } @@ -276,6 +294,9 @@ function Get-ChannelForgeOneGuide { [Array]::Copy($allOfferings, $offerings, $offeringLimit) } else { $offerings = $allOfferings } + if ($group.HasGenericSports -and $group.HasSpecificSport) { + [void]$group.CategoryKeys.Remove('other-sports') + } $orderedCategories = [System.Collections.Generic.List[string]]::new() foreach ($taxonomyKey in $taxonomyOrder) { if ($group.CategoryKeys.Contains($taxonomyKey)) { [void]$orderedCategories.Add($taxonomyKey) } diff --git a/tests/unit/OneGuide.Tests.ps1 b/tests/unit/OneGuide.Tests.ps1 index eea88e1..c630241 100644 --- a/tests/unit/OneGuide.Tests.ps1 +++ b/tests/unit/OneGuide.Tests.ps1 @@ -59,42 +59,65 @@ Describe 'One Guide read projection' { $live.EvaluationTimeUtc | Should -Be $Evaluation.ToString('o') } - It 'exposes only the eight Slice 1 categories and keeps wrestling promotion structured' { + It 'exposes the centralized category taxonomy and keeps wrestling promotion structured' { $fixtureProgrammes = Get-OneGuideFixtureProgrammes - $expectedTaxonomy = @('live-now', 'starting-soon', 'wrestling', 'football', 'baseball', 'soccer', 'movies', 'news') - $registryTaxonomy = @(& (Get-Module ChannelForge) { Get-ChannelForgeOneGuideCategoryRegistry } | ForEach-Object { $_.Key }) + $expectedTaxonomy = @('live-now', 'starting-soon', 'football', 'baseball', 'basketball', 'hockey', 'soccer', 'wrestling', 'motorsports', 'boxing', 'mma', 'tennis', 'golf', 'rugby', 'cricket', 'lacrosse', 'other-sports', 'movies', 'news', 'kids', 'entertainment', 'documentary', 'comedy') + $registry = @(& (Get-Module ChannelForge) { Get-ChannelForgeOneGuideCategoryRegistry }) + $registryTaxonomy = @($registry | ForEach-Object { $_.Key }) $registryTaxonomy | Should -Be $expectedTaxonomy - $categoryAliases = [ordered]@{ + $aliasCases = [ordered]@{ wrestling = 'Professional Wrestling' football = 'Football' baseball = 'Baseball' soccer = 'Association Football' movies = 'Film' news = 'Current Affairs' + hockey = 'Ice Hockey' + motorsports = 'Auto Racing' + mma = 'Mixed Martial Arts' + kids = "Children's" } - $categoryProgrammes = @( - foreach ($entry in $categoryAliases.GetEnumerator()) { + $programmes = @( + foreach ($entry in $aliasCases.GetEnumerator()) { New-OneGuideTestProgramme "category-$($entry.Key)" ($Evaluation.AddMinutes(30)) ($Evaluation.AddHours(1)) "Category $($entry.Key)" @([string]$entry.Value) } ) - $programmes = @($fixtureProgrammes) + @($categoryProgrammes) - + $programmes += New-OneGuideTestProgramme 'generic-sport' ($Evaluation.AddMinutes(30)) ($Evaluation.AddHours(1)) 'Generic Sport' @('Sports', 'Unknown category') + $programmes += New-OneGuideTestProgramme 'specific-and-generic' ($Evaluation.AddMinutes(30)) ($Evaluation.AddHours(1)) 'Specific and generic' @('Sports', 'Ice Hockey') foreach ($key in $expectedTaxonomy) { $result = Get-ChannelForgeOneGuide -Query Category -CategoryKey $key -Programmes $programmes -EvaluationTimeUtc $Evaluation $result.Query | Should -Be 'Category' $result.CategoryKey | Should -Be $key - $result.TotalCount | Should -BeGreaterThan 0 } - - { Get-ChannelForgeOneGuide -Query Category -CategoryKey hockey -Programmes $programmes -EvaluationTimeUtc $Evaluation } | Should -Throw '*Unsupported One Guide category*' - $items = Get-ChannelForgeOneGuide -Query StartingSoon -Programmes $fixtureProgrammes -EvaluationTimeUtc $Evaluation + $hockey = Get-ChannelForgeOneGuide -Query Category -CategoryKey hockey -Programmes $programmes -EvaluationTimeUtc $Evaluation + $hockey.TotalCount | Should -Be 2 + $otherSports = Get-ChannelForgeOneGuide -Query Category -CategoryKey other-sports -Programmes $programmes -EvaluationTimeUtc $Evaluation + @($otherSports.Items.Title) | Should -Be @('Generic Sport') + $unknownOnly = New-OneGuideTestProgramme 'unknown' ($Evaluation.AddMinutes(30)) ($Evaluation.AddHours(1)) 'Unknown only' @('Unmapped category') + $unknownProjection = Get-ChannelForgeOneGuide -Query StartingSoon -Programmes @($unknownOnly) -EvaluationTimeUtc $Evaluation + $unknownProjection.Items[0].CategoryKeys | Should -Be @('starting-soon') $wrestling = Get-ChannelForgeOneGuide -Query Category -CategoryKey wrestling -Programmes $fixtureProgrammes -EvaluationTimeUtc $Evaluation - $items.Items[0].Promotion | Should -BeNullOrEmpty $wrestling.Items[0].Promotion.Id | Should -Match '^cf-[a-f0-9]{64}$' $wrestling.Items[0].Promotion.Name | Should -Be 'WWE' @($wrestling.Items[0].CategoryKeys | Where-Object { $_ -notin $expectedTaxonomy }) | Should -BeNullOrEmpty } + It 'suppresses generic Sports for a canonical event with a specific sport on another offering' { + $genericRow = New-OneGuideTestProgramme 'generic-channel' ($Evaluation.AddMinutes(30)) ($Evaluation.AddHours(1)) 'Cross-source event' @('Sports') 'provider-a' + $specificRow = New-OneGuideTestProgramme 'football-channel' ($Evaluation.AddMinutes(30)) ($Evaluation.AddHours(1)) 'Cross-source event' @('Football') 'provider-b' + $genericRow | Add-Member -NotePropertyName CanonicalEventId -NotePropertyValue 'shared-sports-event' + $specificRow | Add-Member -NotePropertyName CanonicalEventId -NotePropertyValue 'shared-sports-event' + + $football = Get-ChannelForgeOneGuide -Query Category -CategoryKey football -Programmes @($genericRow, $specificRow) -EvaluationTimeUtc $Evaluation + $otherSports = Get-ChannelForgeOneGuide -Query Category -CategoryKey other-sports -Programmes @($genericRow, $specificRow) -EvaluationTimeUtc $Evaluation + + $football.TotalCount | Should -Be 1 + $football.Items[0].OfferingCount | Should -Be 2 + $football.Items[0].CategoryKeys | Should -Contain 'football' + $football.Items[0].CategoryKeys | Should -Not -Contain 'other-sports' + $otherSports.TotalCount | Should -Be 0 + } + It 'groups offerings only when an explicit canonical event identity is present' { $programmes = Get-OneGuideFixtureProgrammes $reversed = @($programmes) diff --git a/tests/unit/WebServer.Tests.ps1 b/tests/unit/WebServer.Tests.ps1 index e363382..6fd0a51 100644 --- a/tests/unit/WebServer.Tests.ps1 +++ b/tests/unit/WebServer.Tests.ps1 @@ -553,16 +553,20 @@ Describe 'ChannelForge web server foundation' { $payload = $live.Body | ConvertFrom-Json $details = Get-ChannelForgeWebResponse -Method HEAD -Path ("/api/one-guide/items/{0}" -f $payload.Items[0].ItemId) -RepositoryRoot $RepositoryRoot $category = Get-ChannelForgeWebResponse -Method GET -Path '/api/one-guide/category/news' -RepositoryRoot $RepositoryRoot + $hockeyCategory = Get-ChannelForgeWebResponse -Method GET -Path '/api/one-guide/category/hockey' -RepositoryRoot $RepositoryRoot + $motorsportsCategory = Get-ChannelForgeWebResponse -Method GET -Path '/api/one-guide/category/motorsports' -RepositoryRoot $RepositoryRoot $invalidCategory = Get-ChannelForgeWebResponse -Method GET -Path '/api/one-guide/category/unsupported' -RepositoryRoot $RepositoryRoot $post = Get-ChannelForgeWebResponse -Method POST -Path '/api/one-guide/live-now' -RepositoryRoot $RepositoryRoot $paged = Get-ChannelForgeWebResponse -Method GET -Path '/api/one-guide/live-now?limit=1&offset=0' -RepositoryRoot $RepositoryRoot $invalidQuery = Get-ChannelForgeWebResponse -Method GET -Path '/api/one-guide/live-now?limit=101' -RepositoryRoot $RepositoryRoot - [pscustomobject]@{ Live = $live; Payload = $payload; Details = $details; Category = $category; InvalidCategory = $invalidCategory; Post = $post; Paged = $paged; InvalidQuery = $invalidQuery } + [pscustomobject]@{ Live = $live; Payload = $payload; Details = $details; Category = $category; HockeyCategory = $hockeyCategory; MotorsportsCategory = $motorsportsCategory; InvalidCategory = $invalidCategory; Post = $post; Paged = $paged; InvalidQuery = $invalidQuery } } $root $global:ChannelForgeOneGuideCacheCheckArmed = $false $live = $responses.Live $payload = $responses.Payload $details = $responses.Details + $hockeyCategory = $responses.HockeyCategory + $motorsportsCategory = $responses.MotorsportsCategory $category = $responses.Category $invalidCategory = $responses.InvalidCategory $post = $responses.Post @@ -577,6 +581,10 @@ Describe 'ChannelForge web server foundation' { $payload.Items[0].Title | Should -Be 'Accepted Live News' $live.Body | Should -Not -Match 'private-value|https?://' $details.StatusCode | Should -Be 200 + $hockeyCategory.StatusCode | Should -Be 200 + ($hockeyCategory.Body | ConvertFrom-Json).CategoryKey | Should -Be 'hockey' + $motorsportsCategory.StatusCode | Should -Be 200 + ($motorsportsCategory.Body | ConvertFrom-Json).CategoryKey | Should -Be 'motorsports' ($details.Body | ConvertFrom-Json).Query | Should -Be 'Details' ($category.Body | ConvertFrom-Json).TotalCount | Should -Be 1 $invalidCategory.StatusCode | Should -Be 400 From 492e044a8ab1fa2253b79f126178c43062f6bf75 Mon Sep 17 00:00:00 2001 From: Jumpstile <29765787+Jumpstile@users.noreply.github.com> Date: Thu, 1 Oct 2026 07:00:22 -0400 Subject: [PATCH 2/3] test(one-guide): assert each alias case returns its programme for its category key --- tests/unit/OneGuide.Tests.ps1 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/OneGuide.Tests.ps1 b/tests/unit/OneGuide.Tests.ps1 index c630241..023f753 100644 --- a/tests/unit/OneGuide.Tests.ps1 +++ b/tests/unit/OneGuide.Tests.ps1 @@ -88,6 +88,9 @@ Describe 'One Guide read projection' { $result = Get-ChannelForgeOneGuide -Query Category -CategoryKey $key -Programmes $programmes -EvaluationTimeUtc $Evaluation $result.Query | Should -Be 'Category' $result.CategoryKey | Should -Be $key + if ($aliasCases.Contains($key)) { + @($result.Items.Title) | Should -Contain "Category $key" + } } $hockey = Get-ChannelForgeOneGuide -Query Category -CategoryKey hockey -Programmes $programmes -EvaluationTimeUtc $Evaluation $hockey.TotalCount | Should -Be 2 From 0055211fb2f11b3838ecc7a1c7b17726ffa0fb73 Mon Sep 17 00:00:00 2001 From: Jumpstile <29765787+Jumpstile@users.noreply.github.com> Date: Thu, 1 Oct 2026 11:35:04 -0400 Subject: [PATCH 3/3] fix(one-guide): treat Other Sports alias as generic sports for precedence (#191) --- src/ChannelForge/Public/Get-ChannelForgeOneGuide.ps1 | 1 + tests/unit/OneGuide.Tests.ps1 | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/ChannelForge/Public/Get-ChannelForgeOneGuide.ps1 b/src/ChannelForge/Public/Get-ChannelForgeOneGuide.ps1 index ecfd22e..4d31507 100644 --- a/src/ChannelForge/Public/Get-ChannelForgeOneGuide.ps1 +++ b/src/ChannelForge/Public/Get-ChannelForgeOneGuide.ps1 @@ -103,6 +103,7 @@ function Get-ChannelForgeOneGuide { if ($key -in @('sport', 'sports')) { $hasGenericSports = $true; continue } if ($taxonomy.ContainsKey($key)) { $mappedCategoryKey = [string]$taxonomy[$key] + if ($mappedCategoryKey -ceq 'other-sports') { $hasGenericSports = $true; continue } [void]$categories.Add($mappedCategoryKey) if ($specificSportKeys.ContainsKey($mappedCategoryKey)) { $hasSpecificSport = $true } } diff --git a/tests/unit/OneGuide.Tests.ps1 b/tests/unit/OneGuide.Tests.ps1 index 023f753..109a434 100644 --- a/tests/unit/OneGuide.Tests.ps1 +++ b/tests/unit/OneGuide.Tests.ps1 @@ -84,6 +84,8 @@ Describe 'One Guide read projection' { ) $programmes += New-OneGuideTestProgramme 'generic-sport' ($Evaluation.AddMinutes(30)) ($Evaluation.AddHours(1)) 'Generic Sport' @('Sports', 'Unknown category') $programmes += New-OneGuideTestProgramme 'specific-and-generic' ($Evaluation.AddMinutes(30)) ($Evaluation.AddHours(1)) 'Specific and generic' @('Sports', 'Ice Hockey') + $programmes += New-OneGuideTestProgramme 'other-sports-label' ($Evaluation.AddMinutes(30)) ($Evaluation.AddHours(1)) 'Other Sports label' @('Other Sports') + $programmes += New-OneGuideTestProgramme 'other-sports-and-specific' ($Evaluation.AddMinutes(30)) ($Evaluation.AddHours(1)) 'Other Sports and specific' @('Other Sports', 'Football') foreach ($key in $expectedTaxonomy) { $result = Get-ChannelForgeOneGuide -Query Category -CategoryKey $key -Programmes $programmes -EvaluationTimeUtc $Evaluation $result.Query | Should -Be 'Category' @@ -95,7 +97,9 @@ Describe 'One Guide read projection' { $hockey = Get-ChannelForgeOneGuide -Query Category -CategoryKey hockey -Programmes $programmes -EvaluationTimeUtc $Evaluation $hockey.TotalCount | Should -Be 2 $otherSports = Get-ChannelForgeOneGuide -Query Category -CategoryKey other-sports -Programmes $programmes -EvaluationTimeUtc $Evaluation - @($otherSports.Items.Title) | Should -Be @('Generic Sport') + @($otherSports.Items.Title) | Should -Be @('Generic Sport', 'Other Sports label') + $football = Get-ChannelForgeOneGuide -Query Category -CategoryKey football -Programmes $programmes -EvaluationTimeUtc $Evaluation + @($football.Items.Title) | Should -Contain 'Other Sports and specific' $unknownOnly = New-OneGuideTestProgramme 'unknown' ($Evaluation.AddMinutes(30)) ($Evaluation.AddHours(1)) 'Unknown only' @('Unmapped category') $unknownProjection = Get-ChannelForgeOneGuide -Query StartingSoon -Programmes @($unknownOnly) -EvaluationTimeUtc $Evaluation $unknownProjection.Items[0].CategoryKeys | Should -Be @('starting-soon')