exhaustive tests for sequencer function
All checks were successful
Unit tests / Unit tests (push) Successful in -2m10s
NPM Audit Check / Check NPM audit (push) Successful in -2m17s

This commit is contained in:
2025-08-29 11:11:16 -05:00
parent e6964315ab
commit 0c3b5c3be5
3 changed files with 101 additions and 15 deletions

View File

@@ -21,7 +21,7 @@
"main": "distribution/index.js", "main": "distribution/index.js",
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"start": "node distribution/index.js", "start": "node distribution/src/index.js",
"test": "vitest" "test": "vitest"
}, },
"dependencies": { "dependencies": {

View File

@@ -1,4 +1,4 @@
import { OpenWeatherAPI, type CurrentWeather } from 'openweather-api-node'; import { OpenWeatherAPI, type DailyWeather } from 'openweather-api-node';
import { voiceLines } from './voice.js'; import { voiceLines } from './voice.js';
import type { Config } from './index.js'; import type { Config } from './index.js';
import type { Voice } from './voice.js'; import type { Voice } from './voice.js';
@@ -19,7 +19,7 @@ function selectOne<T>(arr: T[]): T {
return arr[Math.floor(Math.random() * arr.length)]; return arr[Math.floor(Math.random() * arr.length)];
} }
function resolveSide(side: string, currentWeather: CurrentWeather) { function resolveSide(side: string, currentWeather: DailyWeather) {
if (!side.startsWith('weather')) { if (!side.startsWith('weather')) {
return side.includes('.') ? parseFloat(side) : parseInt(side); return side.includes('.') ? parseFloat(side) : parseInt(side);
} }
@@ -30,13 +30,23 @@ function resolveSide(side: string, currentWeather: CurrentWeather) {
return typeof w === 'object' ? JSON.stringify(w) : w as (string | number); return typeof w === 'object' ? JSON.stringify(w) : w as (string | number);
} }
function conditionIsMet(condition: string | undefined, currentWeather: CurrentWeather): boolean { function notNotANumber(something: number | string, defaultVal: string) {
if (typeof something === 'string' || !isNaN(something)) {
return something;
}
return defaultVal;
}
function conditionIsMet(condition: string | undefined, currentWeather: DailyWeather): boolean {
if (typeof condition !== 'string') { if (typeof condition !== 'string') {
return true; return true;
} }
const [lhs, relational, rhs] = condition.split(' '); const [lhs, relational, rhs] = condition.split(' ');
const lhsResolved = resolveSide(lhs, currentWeather); if (lhs === undefined || relational === undefined || rhs === undefined) {
const rhsResolved = resolveSide(rhs, currentWeather); throw new Error(`Condition "${condition}" is not in the correct format`);
}
const lhsResolved = notNotANumber(resolveSide(lhs, currentWeather), lhs);
const rhsResolved = notNotANumber(resolveSide(rhs, currentWeather), rhs);
switch (relational) { switch (relational) {
case '=': case '=':
case '==': case '==':
@@ -56,7 +66,7 @@ function conditionIsMet(condition: string | undefined, currentWeather: CurrentWe
} }
} }
function resolveMacro(str: string, currentWeather: CurrentWeather): string[] { function resolveMacro(str: string, currentWeather: DailyWeather): string[] {
if (str.startsWith('%')) { if (str.startsWith('%')) {
const [profile, subject] = str.substring(1).split(' ', 2); const [profile, subject] = str.substring(1).split(' ', 2);
const voiceProfile: Voice = config.voices[profile]; const voiceProfile: Voice = config.voices[profile];
@@ -64,18 +74,15 @@ function resolveMacro(str: string, currentWeather: CurrentWeather): string[] {
subject.split('.').forEach(t => resolvedSubject = resolvedSubject[t]); subject.split('.').forEach(t => resolvedSubject = resolvedSubject[t]);
return voiceLines(voiceProfile, resolvedSubject); return voiceLines(voiceProfile, resolvedSubject);
} }
else if (str.startsWith('$')) {
return null;
}
return [str]; return [str];
} }
function processSequence(sequence: Sequence, currentWeather: CurrentWeather): string[] { function processSequence(sequence: Sequence, currentWeather: DailyWeather): string[] {
const tracks = sequence.tracks; const tracks = sequence.tracks;
return tracks.map(t => resolveMacro(t, currentWeather)).flat().filter(t => t !== null); return tracks.map(t => resolveMacro(t, currentWeather)).flat().filter(t => t !== null);
} }
function processSegment(segment: SegmentName, currentWeather: CurrentWeather): string[] { function processSegment(segment: SegmentName, currentWeather: DailyWeather): string[] {
if (!(segment in config.segments)) { if (!(segment in config.segments)) {
return (config.sequences[segment].conditions || []).every(c => conditionIsMet(c, currentWeather)) ? processSequence(config.sequences[segment], currentWeather) : []; return (config.sequences[segment].conditions || []).every(c => conditionIsMet(c, currentWeather)) ? processSequence(config.sequences[segment], currentWeather) : [];
} }
@@ -89,8 +96,7 @@ function processSegment(segment: SegmentName, currentWeather: CurrentWeather): s
async function Sequencer(conf: Config): Promise<string[]> { async function Sequencer(conf: Config): Promise<string[]> {
config = conf; config = conf;
const weather = new OpenWeatherAPI(conf.weather); const weather = new OpenWeatherAPI(conf.weather);
const currentWeather = await weather.getCurrent(); const currentWeather = await weather.getToday();
console.log(JSON.stringify(currentWeather));
const sequence: string[] = []; const sequence: string[] = [];
const program: SegmentName[] = selectOne(conf.programs); const program: SegmentName[] = selectOne(conf.programs);
for (let i = 0; i < program.length; i++) { for (let i = 0; i < program.length; i++) {

View File

@@ -9,7 +9,7 @@ vi.mock('openweather-api-node', () => {
return { return {
OpenWeatherAPI: vi.fn().mockImplementation((_) => { OpenWeatherAPI: vi.fn().mockImplementation((_) => {
return { return {
getCurrent: vi.fn(() => { getToday: vi.fn(() => {
return { return {
"lat": 43.0748, "lat": 43.0748,
"lon": -89.3838, "lon": -89.3838,
@@ -106,5 +106,85 @@ describe('sequencer', () => {
weather: dummyWeather weather: dummyWeather
})).to.be.ordered.members(['seq1.flac', 'seq1.flac']); })).to.be.ordered.members(['seq1.flac', 'seq1.flac']);
}); });
it('throws an error on invalid conditions', async () => {
await expect(Sequencer({
programs: [['sequence 1']],
segments: {
},
sequences: {
'sequence 1': {
'conditions': ['100'],
'tracks': [
'seq1.flac'
]
}
},
voices: {},
weather: dummyWeather
})).rejects.toThrow(/not in the correct format/);
await expect(Sequencer({
programs: [['sequence 1']],
segments: {
},
sequences: {
'sequence 1': {
'conditions': ['1 ~ 2'],
'tracks': [
'seq1.flac'
]
}
},
voices: {},
weather: dummyWeather
})).rejects.toThrow(/Unsupported relational operator/);
});
it('can stringify conditions', async () => {
expect(await Sequencer({
programs: [['sequence 1']],
segments: {
},
sequences: {
'sequence 1': {
'conditions': ['weather.feelsLike = {"cur":55.31}'],
'tracks': [
'seq1.flac'
]
}
},
voices: {},
weather: dummyWeather
})).to.be.ordered.members(['seq1.flac']);
});
it('can parse voice macros', async () => {
expect(await Sequencer({
programs: [['sequence 1']],
segments: {
},
sequences: {
'sequence 1': {
'tracks': [
'%alice weather.temp.max'
]
}
},
voices: {
"alice": {
"directory": "alice/",
"extension": "flac"
}
},
weather: dummyWeather
})).to.be.ordered.members([
'alice/fifty.flac',
'alice/eight.flac',
'alice/point.flac',
'alice/zero.flac',
'alice/one.flac'
]);
});
}); });