All files / agent/src acceptanceChecker.ts

100% Statements 83/83
100% Branches 26/26
100% Functions 4/4
100% Lines 83/83

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 841x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 41x 41x 41x 41x 41x 41x 41x 41x 41x 1x 38x 38x 38x 1x 1x 1x 1x 1x 1x 1x 1x 38x 38x 6x 6x 6x 6x 32x 32x 38x 12x 1x 1x 12x 31x 31x 38x 20x 3x 3x 20x 28x 28x 38x 2x 2x 2x 2x 2x 26x 26x 38x 1x 1x 25x 25x 38x 1x  
/**
 * Acceptance Checker
 *
 * Evaluates whether a job should be accepted based on configured
 * acceptance rules (recipe patterns, repository patterns, duration, Docker).
 */
 
import { AcceptanceRules } from './config/types.js';
 
export interface JobCandidate {
  recipe?: string;
  repository?: string;
  estimatedDurationMinutes?: number;
  requiresDocker?: boolean;
}
 
export interface AcceptanceDecision {
  accepted: boolean;
  reason?: string;
}
 
/**
 * Simple glob matcher supporting * and ? wildcards.
 * Avoids external dependency on minimatch.
 */
function globMatch(pattern: string, value: string): boolean {
  // Convert glob to regex
  const escaped = pattern
    .replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape regex special chars (except * and ?)
    .replace(/\*/g, '.*')
    .replace(/\?/g, '.');
  const regex = new RegExp(`^${escaped}$`, 'i');
  return regex.test(value);
}
 
function matchesAny(patterns: string[], value: string): boolean {
  return patterns.some(p => globMatch(p, value));
}
 
export class AcceptanceChecker {
  constructor(private rules: AcceptanceRules) {}
 
  /**
   * Check if a job should be accepted based on the configured acceptance rules.
   */
  shouldAccept(job: JobCandidate): AcceptanceDecision {
    // Check blocked recipes first (takes precedence)
    if (job.recipe && this.rules.blockedRecipes.length > 0) {
      if (matchesAny(this.rules.blockedRecipes, job.recipe)) {
        return { accepted: false, reason: `Recipe "${job.recipe}" is blocked` };
      }
    }
 
    // Check allowed recipes
    if (job.recipe && this.rules.allowedRecipes.length > 0) {
      if (!matchesAny(this.rules.allowedRecipes, job.recipe)) {
        return { accepted: false, reason: `Recipe "${job.recipe}" is not in allowed list` };
      }
    }
 
    // Check allowed repositories
    if (job.repository && this.rules.allowedRepositories.length > 0) {
      if (!matchesAny(this.rules.allowedRepositories, job.repository)) {
        return { accepted: false, reason: `Repository "${job.repository}" is not in allowed list` };
      }
    }
 
    // Check max duration
    if (job.estimatedDurationMinutes !== undefined && job.estimatedDurationMinutes > this.rules.maxJobDurationMinutes) {
      return {
        accepted: false,
        reason: `Estimated duration (${job.estimatedDurationMinutes}m) exceeds max (${this.rules.maxJobDurationMinutes}m)`
      };
    }
 
    // Check Docker requirement
    if (this.rules.requireDocker && job.requiresDocker === false) {
      return { accepted: false, reason: 'Agent requires Docker but job does not use Docker' };
    }
 
    return { accepted: true };
  }
}