Skip to content
Merged
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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Changelog

## [6.2.9] - 2026-04-28

### Improved

- **Toktrack-2.6.0-Modellnamen mit Provider-Suffixen** — Modelle wie `GPT-5 4::openai` werden jetzt auf die kanonische Modellfamilie und den zugehörigen Provider normalisiert, sodass vordefinierte Farben, Aggregationen und Provider-Erkennung auch mit den neuen Toktrack-Suffixen stabil bleiben
- **CodeQL-stabilere Integrationstest-Helfer** — E2E-/Integrationstest-Fetches akzeptieren nur noch validierte Loopback-URLs, Background-Registry-Fixtures werden strukturiert normalisiert, und lokale Auth-Bootstrap-Tests nutzen explizite Test-Tokens statt file-derived URLs
- **Robustere Reporting-Locale-E2E-Abdeckung** — der PDF-Reporting-Test nutzt stabile Sprachumschalter-Test-IDs und prüft den Report-Payload sowohl im deutschen Ausgangszustand als auch nach dem Wechsel auf Englisch

### Fixed

- **CodeQL-Alerts in Testpfaden** — TOCTOU- und Datei-/Netzwerk-Datenflüsse in Auto-Import-, Local-Auth- und Background-Registry-Tests wurden entfernt oder auf explizit validierte lokale Testdaten begrenzt
- **Fragile Sprachumschalter-Selektion im Reporting-Test** — der E2E-Test hängt nicht mehr an übersetzten `title`-Strings wie `English` oder `Englisch`, sondern an stabilen `data-testid`-Attributen

### Commits

- Enthält alle Branch-Commits seit `6.2.8`: `6fa78ce`, `c2494ae`, `365254b`
Comment thread
tyl3r-ch marked this conversation as resolved.

## [6.2.8] - 2026-04-27

### Added
Expand Down
36 changes: 34 additions & 2 deletions shared/dashboard-domain.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ const PROVIDER_MATCHERS = modelNormalizationSpec.providerMatchers.map((matcher)
matcher: new RegExp(matcher.pattern, 'i'),
}))

const TOKTRACK_PROVIDER_SUFFIXES = new Map([
['alibaba', 'Alibaba'],
['anthropic', 'Anthropic'],
['cohere', 'Cohere'],
['deepseek', 'DeepSeek'],
['google', 'Google'],
['mistral', 'Mistral'],
['opencode', 'OpenCode'],
['openai', 'OpenAI'],
['xai', 'xAI'],
['meta', 'Meta'],
])

function titleCaseSegment(segment) {
if (!segment) return segment
if (/^\d+([.-]\d+)*$/.test(segment)) return segment.replace(/-/g, '.')
Expand All @@ -26,9 +39,25 @@ function formatVersion(version) {
return version.replace(/-/g, '.')
}

function splitToktrackProviderSuffix(raw) {
const value = String(raw || '').trim()
const match = value.match(/^(.*)::([a-z][a-z0-9_-]*)$/i)
if (!match) {
return { model: value, provider: null }
}

const model = match[1].trim()
const provider = TOKTRACK_PROVIDER_SUFFIXES.get(match[2].toLowerCase()) ?? null
if (!model || !provider) {
return { model: value, provider: null }
}

return { model, provider }
}

function canonicalizeModelName(raw) {
const normalized = String(raw || '')
.trim()
const { model } = splitToktrackProviderSuffix(raw)
const normalized = model
.toLowerCase()
.replace(/^model[:/ -]*/i, '')
.replace(/^(anthropic|openai|google|vertex|models)[/-]/i, '')
Expand Down Expand Up @@ -191,6 +220,9 @@ function normalizeModelName(raw) {
* @returns The normalized provider name.
*/
function getModelProvider(raw) {
const suffixProvider = splitToktrackProviderSuffix(raw).provider
if (suffixProvider) return suffixProvider

const canonical = canonicalizeModelName(raw)
for (const matcher of PROVIDER_MATCHERS) {
if (matcher.matcher.test(canonical)) return matcher.provider
Expand Down
1 change: 1 addition & 0 deletions src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ export function Header({
<button
key={language}
type="button"
data-testid={`language-switcher-${language}`}
onClick={() => onLanguageChange(language)}
aria-pressed={currentLanguage === language}
className={`rounded px-2 py-1 text-[10px] font-medium transition-colors ${currentLanguage === language ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}
Expand Down
10 changes: 8 additions & 2 deletions tests/e2e/dashboard-reporting.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@ test('uses the current UI language when generating a PDF report after switching

await gotoDashboard(page)
await uploadSampleUsage(page)
await page.getByTitle(/English|Englisch/).click()
await expect(page.locator('#filters').getByText('Filter status')).toBeVisible()
await page.getByTestId('language-switcher-de').click()
await expect(page.locator('html')).toHaveAttribute('lang', 'de')

await page.getByRole('button', { name: 'Report' }).click()
await expect.poll(() => pdfReport.getReportRequest()?.language).toBe('de')
Comment thread
coderabbitai[bot] marked this conversation as resolved.

await page.getByTestId('language-switcher-en').click()
await expect(page.locator('html')).toHaveAttribute('lang', 'en')
await expect(page.locator('#filters').getByText('Filter status')).toBeVisible()

await page.getByRole('button', { name: 'Report' }).click()
await expect.poll(() => pdfReport.getReportRequest()?.language).toBe('en')
})
4 changes: 1 addition & 3 deletions tests/integration/server-auto-import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,7 @@ describe('local server auto-import integration', () => {
expect(firstStreamBody).toContain('event: done')
expect(readFileSync(invocationCountPath, 'utf-8')).toBe('1')
} finally {
if (!existsSync(releaseRunnerPath)) {
writeFileSync(releaseRunnerPath, 'release')
}
writeFileSync(releaseRunnerPath, 'release')
if (standaloneServer) await stopProcess(standaloneServer.child)
rmSync(runtimeRoot, { recursive: true, force: true })
}
Expand Down
33 changes: 27 additions & 6 deletions tests/integration/server-background-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { describe, expect, it } from 'vitest'
import {
createCliEnv,
createSharedServerContext,
fetchWithAuth,
readBackgroundRegistry,
registerSharedServerLifecycle,
runCli,
Expand All @@ -21,23 +20,23 @@ describe('local server background registry pruning', () => {
const backgroundEnv = createCliEnv(backgroundRoot)

try {
const runtimeResponse = await fetchWithAuth(`${sharedServer.baseUrl}/api/runtime`)
const runtime = await runtimeResponse.json()
const sharedServerPid = sharedServer.child?.pid
if (!sharedServerPid) {
throw new Error('Shared server child process was not started.')
}
const sharedServerOrigin = new URL(sharedServer.baseUrl).origin
const sharedServerPort = Number.parseInt(new URL(sharedServer.baseUrl).port, 10)

writeBackgroundRegistry(backgroundRoot, [
{
id: 'stale-entry',
pid: sharedServerPid,
port: runtime.port,
url: sharedServer.baseUrl,
port: sharedServerPort,
url: sharedServerOrigin,
host: '127.0.0.1',
authHeader: sharedServer.authHeader,
startedAt: new Date().toISOString(),
logFile: null,
...(sharedServer.authHeader ? { authHeader: sharedServer.authHeader } : {}),
},
])

Expand All @@ -48,4 +47,26 @@ describe('local server background registry pruning', () => {
rmSync(backgroundRoot, { recursive: true, force: true })
}
}, 15_000)

it('rejects registry fixtures that would be rewritten by normalization', () => {
const backgroundRoot = mkdtempSync(path.join(tmpdir(), 'ttdash-background-invalid-test-'))

try {
expect(() =>
writeBackgroundRegistry(backgroundRoot, [
{
id: 'rewritten-entry',
pid: process.pid,
port: 3011,
url: 'http://127.0.0.1:3011/dashboard',
host: '127.0.0.1',
startedAt: new Date().toISOString(),
logFile: null,
},
]),
).toThrow('Invalid test background registry entry at index 0.')
} finally {
rmSync(backgroundRoot, { recursive: true, force: true })
}
})
})
21 changes: 17 additions & 4 deletions tests/integration/server-local-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { tmpdir } from 'node:os'
import path from 'node:path'
import { describe, expect, it } from 'vitest'
import {
fetchLocalBootstrap,
fetchTrusted,
getLocalAuthSessionPath,
isPosix,
Expand Down Expand Up @@ -46,10 +47,19 @@ describe('local server session authentication', () => {

it('protects loopback read APIs and accepts bearer or bootstrap cookie credentials', async () => {
const runtimeRoot = mkdtempSync(path.join(tmpdir(), 'ttdash-local-auth-test-'))
const localToken = 'ttdash-local-auth-bootstrap-token-123456'
let standaloneServer: Awaited<ReturnType<typeof startStandaloneServer>> | null = null

try {
standaloneServer = await startStandaloneServer({ root: runtimeRoot })
standaloneServer = await startStandaloneServer({
root: runtimeRoot,
envOverrides: {
TTDASH_LOCAL_AUTH_TOKEN: localToken,
},
readinessHeaders: {
Authorization: createBearerAuthHeader(localToken),
},
})

for (const apiPath of [
'/api/usage',
Expand All @@ -66,9 +76,12 @@ describe('local server session authentication', () => {
expect(authenticatedResponse.status).toBe(200)
}

const bootstrapResponse = await fetch(standaloneServer.bootstrapUrl!, {
redirect: 'manual',
})
const bootstrapResponse = await fetchLocalBootstrap(
`${standaloneServer.url}/?ttdash_token=${localToken}`,
{
redirect: 'manual',
},
)
expect(bootstrapResponse.status).toBe(303)
expect(bootstrapResponse.headers.get('location')).toBe('/')
const cookieHeader = bootstrapResponse.headers.get('set-cookie')?.split(';', 1)[0]
Expand Down
Loading
Loading