feat: rewrite APK parser to use version as delimiter

- Changed parseApkFilename to accept version parameter
- New parsing logic splits filename by version string to extract prefix
- detectEnvironment now takes prefix and configured tabs as arguments
- Environments not in APK_TABS are mapped to 'other' instead of hardcoded mapping
- Added getApkTabs helper function to expose configured tabs
- Updated all tests to use new API with version parameter

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 23:33:45 +08:00
parent 10fa6782e6
commit df4daea1c7
2 changed files with 78 additions and 49 deletions

View File

@@ -1,60 +1,69 @@
import { parseApkFilename, detectEnvironment } from '../apk-parser';
import { parseApkFilename, detectEnvironment, getApkTabs } from '../apk-parser';
describe('parseApkFilename', () => {
it('should parse APK with environment suffix', () => {
const result = parseApkFilename('ft_timeshift_0_42_57ef3a60d.apk');
expect(result.environment).toBe('dev');
expect(result.commit).toBe('57ef3a60d');
it('should parse product APK (no environment)', () => {
const result = parseApkFilename('ft_0_44_bfeddf2d0.apk', '0_44');
expect(result.environment).toBe('product');
expect(result.commit).toBe('bfeddf2d0');
expect(result.isValid).toBe(true);
});
it('should parse APK without environment (product)', () => {
const result = parseApkFilename('ft_0_42_ff9ff3441.apk');
expect(result.environment).toBe('product');
expect(result.commit).toBe('ff9ff3441');
it('should parse dev APK', () => {
const result = parseApkFilename('ft_dev_0_44_bfeddf2d0.apk', '0_44');
expect(result.environment).toBe('dev');
expect(result.commit).toBe('bfeddf2d0');
expect(result.isValid).toBe(true);
});
it('should parse sandbox APK', () => {
const result = parseApkFilename('ft_sandbox_0_42_57ef3a60d.apk');
const result = parseApkFilename('ft_sandbox_0_44_bfeddf2d0.apk', '0_44');
expect(result.environment).toBe('sandbox');
expect(result.commit).toBe('57ef3a60d');
expect(result.commit).toBe('bfeddf2d0');
expect(result.isValid).toBe(true);
});
it('should handle invalid format', () => {
const result = parseApkFilename('invalid_filename.apk');
expect(result.environment).toBe('product');
it('should map unknown environment to other', () => {
const result = parseApkFilename('ft_lan_0_44_bfeddf2d0.apk', '0_44');
expect(result.environment).toBe('other');
expect(result.commit).toBe('bfeddf2d0');
});
it('should map timeshift to other', () => {
const result = parseApkFilename('ft_timeshift_0_42_57ef3a60d.apk', '0_42');
expect(result.environment).toBe('other');
});
it('should handle invalid format without commit', () => {
const result = parseApkFilename('ft_dev_0_44.apk', '0_44');
expect(result.environment).toBe('dev');
expect(result.commit).toBeNull();
expect(result.isValid).toBe(false);
});
it('should handle missing commit ID', () => {
const result = parseApkFilename('ft_dev_0_42.apk');
expect(result.environment).toBe('dev');
it('should handle completely invalid filename', () => {
const result = parseApkFilename('invalid_filename.apk', '0_44');
expect(result.environment).toBe('other');
expect(result.commit).toBeNull();
expect(result.isValid).toBe(false);
});
});
describe('detectEnvironment', () => {
it('should detect dev environment', () => {
expect(detectEnvironment('ft_timeshift_0_42_57ef3a60d.apk')).toBe('dev');
it('should detect product when no underscore in prefix', () => {
expect(detectEnvironment('ft', ['dev', 'sandbox'])).toBe('product');
});
it('should detect sandbox environment', () => {
expect(detectEnvironment('ft_sandbox_0_42_57ef3a60d.apk')).toBe('sandbox');
it('should detect configured environment', () => {
expect(detectEnvironment('ft_dev', ['dev', 'sandbox'])).toBe('dev');
expect(detectEnvironment('ft_sandbox', ['dev', 'sandbox'])).toBe('sandbox');
});
it('should detect product environment (no suffix)', () => {
expect(detectEnvironment('ft_0_42_ff9ff3441.apk')).toBe('product');
it('should return other for unconfigured environment', () => {
expect(detectEnvironment('ft_lan', ['dev', 'sandbox'])).toBe('other');
expect(detectEnvironment('ft_timeshift', ['dev', 'sandbox'])).toBe('other');
});
it('should detect lan environment', () => {
expect(detectEnvironment('ft_lan_0_42_57ef3a60d.apk')).toBe('dev');
});
it('should return other for unknown environments', () => {
expect(detectEnvironment('ft_unknown_0_42_57ef3a60d.apk')).toBe('other');
it('should return other for empty prefix', () => {
expect(detectEnvironment('', ['dev', 'sandbox'])).toBe('other');
});
});

View File

@@ -1,4 +1,6 @@
export type ApkEnvironment = 'dev' | 'sandbox' | 'product' | 'other';
import { APK_TABS } from './constants';
export type ApkEnvironment = 'product' | 'dev' | 'sandbox' | 'other';
export interface ParsedApkMetadata {
environment: ApkEnvironment;
@@ -6,37 +8,55 @@ export interface ParsedApkMetadata {
isValid: boolean;
}
// Environment mapping based on filename patterns
const ENVIRONMENT_MAP: Record<string, ApkEnvironment> = {
'timeshift': 'dev',
'lan': 'dev',
'dev': 'dev',
'sandbox': 'sandbox',
};
export function getApkTabs(): string[] {
return APK_TABS;
}
export function detectEnvironment(filename: string): ApkEnvironment {
// Pattern: ft_[environment]_version_commit.apk
// Extract the environment segment (alphabetic only) between first and second underscores
const match = filename.match(/^ft_([a-z]+)_/i);
export function detectEnvironment(prefix: string, configuredTabs: string[]): ApkEnvironment {
// Empty prefix → other
if (!prefix) {
return 'other';
}
if (!match) {
// No environment suffix, default to product
// No underscore means product (e.g., "ft")
if (!prefix.includes('_')) {
return 'product';
}
const env = match[1].toLowerCase();
return ENVIRONMENT_MAP[env] || 'other';
// Extract environment from last segment after underscore
const lastUnderscoreIndex = prefix.lastIndexOf('_');
const env = prefix.substring(lastUnderscoreIndex + 1);
// Check if environment is configured
if (configuredTabs.includes(env)) {
return env as ApkEnvironment;
}
export function parseApkFilename(filename: string): ParsedApkMetadata {
const environment = detectEnvironment(filename);
return 'other';
}
export function parseApkFilename(filename: string, version: string): ParsedApkMetadata {
// Use version as delimiter (with leading underscore)
const delimiter = `_${version}`;
const delimiterIndex = filename.indexOf(delimiter);
let prefix: string;
if (delimiterIndex > 0) {
prefix = filename.substring(0, delimiterIndex);
} else {
// Fallback: can't find version delimiter
prefix = '';
}
const environment = detectEnvironment(prefix, APK_TABS);
// Extract commit ID
const commitMatch = filename.match(/([a-f0-9]{7,})\.apk$/);
const commit = commitMatch ? commitMatch[1] : null;
const isValid = commit !== null;
return {
environment,
commit,
isValid,
isValid: commit !== null,
};
}