diff --git a/src/main/scala/ccas/analysis/apps/recruitment/RecruitmentApp.scala b/src/main/scala/ccas/analysis/apps/recruitment/RecruitmentApp.scala index 7f73a9f..9623c88 100644 --- a/src/main/scala/ccas/analysis/apps/recruitment/RecruitmentApp.scala +++ b/src/main/scala/ccas/analysis/apps/recruitment/RecruitmentApp.scala @@ -95,7 +95,10 @@ object RecruitmentApp extends ZIOAppDefault { if (parsed.cumulative) RecruitmentCandidate.selectInvitedToday(run.clubId, parsed.alias) else RecruitmentCandidate.selectInvitedByRun(run.runId) resolvedMap <- Player.resolveUsernames(candidates.map(_.playerId)) - usernames = candidates.map(c => resolvedMap.getOrElse(c.playerId, Username.wrap(s"[pid=${c.playerId}]"))) + // Drop any player_id that doesn't resolve to a handle — these lists are paste-ready invite targets, and a + // `[pid=N]` placeholder isn't invitable. Matches the server's `usernamesFor` so the file and the + // --report/clipboard output are identical. + usernames = candidates.flatMap(c => resolvedMap.get(c.playerId)) evaluatedCount <- RecruitmentCandidate.selectCountByRun(run.runId) now <- Clock.instant output = formatRecruitmentOutput( @@ -162,7 +165,7 @@ object RecruitmentApp extends ZIOAppDefault { else ZIO.succeed(0) effectiveTarget = (resolvedTarget - alreadyFound) max 0 now <- Clock.instant - runId <- RecruitmentRun.insert(clubId, criteria.criteriaId, trigger, now, jobRunId) + runId <- RecruitmentRun.insert(clubId, criteria.criteriaId, trigger, now, Some(effectiveTarget), jobRunId) // --- Shared setup --- targetMembers <- ApiClubMembers.get(client, effectiveSlug) @@ -401,6 +404,7 @@ object RecruitmentApp extends ZIOAppDefault { startedAt, Some(completedAt), confirmed.size, + Some(ctx.target), jobRunId ) _ <- RecruitmentRun.update(finalRun) @@ -484,12 +488,15 @@ object RecruitmentApp extends ZIOAppDefault { } invited <- RecruitmentCandidate.selectInvitedByRun(run.runId) evaluatedCount <- RecruitmentCandidate.selectCountByRun(run.runId) - _ <- ZIO.logInfo(s"=== Recruitment Report for $clubSlug (run ${run.runId}) ===") - _ <- ZIO.logInfo(s"Started: ${run.startedAt}") - _ <- ZIO.logInfo(s"Completed: ${run.completedAt.getOrElse("in progress")}") - _ <- ZIO.logInfo(s"Evaluated: $evaluatedCount | Invited: ${invited.size}") resolvedMap <- Player.resolveUsernames(invited.map(_.playerId)) - usernames = invited.map(c => resolvedMap.getOrElse(c.playerId, Username.wrap(s"[pid=${c.playerId}]"))) + // Drop unresolved player_ids (see the scout path): paste-ready invite list, identical to the server's + // `usernamesFor`, so `report`'s file matches `--report`/clipboard. + usernames = invited.flatMap(c => resolvedMap.get(c.playerId)) + _ <- ZIO.logInfo(s"=== Recruitment Report for $clubSlug (run ${run.runId}) ===") + _ <- ZIO.logInfo(s"Started: ${run.startedAt}") + _ <- ZIO.logInfo(s"Completed: ${run.completedAt.getOrElse("in progress")}") + // Report the paste-ready count (post-drop), matching the file's stat line and the --report/clipboard payload. + _ <- ZIO.logInfo(s"Evaluated: $evaluatedCount | Invited: ${usernames.size}") _ <- ZIO.logInfo(usernames.mkString(" ")) _ <- ZIO.logInfo("") _ <- ZIO.foreachDiscard(usernames) { name => diff --git a/src/main/scala/ccas/analysis/apps/recruitment/RecruitmentExplore.scala b/src/main/scala/ccas/analysis/apps/recruitment/RecruitmentExplore.scala index 31da37c..f0e7386 100644 --- a/src/main/scala/ccas/analysis/apps/recruitment/RecruitmentExplore.scala +++ b/src/main/scala/ccas/analysis/apps/recruitment/RecruitmentExplore.scala @@ -217,8 +217,10 @@ private[recruitment] object RecruitmentExplore { ) } yield () - /** When found count exceeds the target, trim the newest excess from invitedRef. All candidates are already Deferred - * in the DB; no status flip needed here. + /** When found count exceeds the target, trim the newest excess from invitedRef so the loop stops and the auto-confirm + * `found` list is capped. The excess candidates stay Deferred in the DB (carried to the next run via + * `selectDeferredByClub`); the interactive confirm path caps its flip at the run's target, so a chunk overshoot never + * invites beyond target. No DB write is needed here. */ def reclassifyExcessInvited(ctx: ExploreContext): RIO[PostgresClient, Unit] = // invitedRef is prepend-ordered (newest first), so drop excess from the head diff --git a/src/main/scala/ccas/analysis/tables/RecruitmentCandidate.scala b/src/main/scala/ccas/analysis/tables/RecruitmentCandidate.scala index 322304f..2e5542f 100644 --- a/src/main/scala/ccas/analysis/tables/RecruitmentCandidate.scala +++ b/src/main/scala/ccas/analysis/tables/RecruitmentCandidate.scala @@ -118,25 +118,47 @@ object RecruitmentCandidate { .query[Int].run().headOption }.someOrFail(new SQLException("COUNT query produced no rows")) - // The run's still-deferred candidates, for the interactive `ccas recruit` confirm prompt (a deferred-confirm run - // leaves everything Deferred; the operator reviews these before any are marked Invited). Ordered by player_id to - // match `selectInvitedByRun`'s deterministic output. + // The run's still-deferred candidates the operator will confirm, for the interactive `ccas recruit` confirm prompt (a + // deferred-confirm run leaves everything Deferred; the operator reviews these before any are marked Invited). Capped + // at the run's remaining budget — `target` minus those already Invited this run — so a chunk overshoot isn't + // shown/confirmed above target and a re-fetch after a partial confirm shows only what's still confirmable. The excess + // stays Deferred and carries to the next run. Legacy runs (NULL target) fall through to `LIMIT NULL` (no cap). Ordered + // by player_id to match `selectInvitedByRun` and `confirmDeferredByRun`. def selectDeferredByRun(runId: RecruitmentRunId): ZIO[PostgresClient, SQLException, List[RecruitmentCandidate]] = connectZIO { val deferred: CandidateOutcome = CandidateOutcome.Deferred + val invited: CandidateOutcome = CandidateOutcome.Invited sql"""SELECT $selectCols FROM recruitment_candidate WHERE run_id = $runId AND outcome = $deferred - ORDER BY player_id""" + ORDER BY player_id + LIMIT (SELECT CASE WHEN rr.target IS NULL THEN NULL + ELSE GREATEST(rr.target - (SELECT COUNT(*) FROM recruitment_candidate + WHERE run_id = $runId AND outcome = $invited), 0) + END + FROM recruitment_run rr WHERE rr.run_id = $runId)""" .query[RecruitmentCandidate].run().toList } - // Confirm a deferred-confirm run: flip its Deferred candidates to Invited in one statement. Returns rows affected - // (0 if already confirmed or nothing found), so the caller can update `recruitment_run.candidates_found`. + // Confirm a deferred-confirm run: flip its remaining-budget Deferred candidates (lowest player_ids first) to Invited. + // Matches `selectDeferredByRun`'s cap+order so the operator invites exactly what the prompt showed. The cap is + // `target - already-invited-this-run`, so a re-POST after a full confirm flips 0 (idempotent, per the endpoint's + // contract) rather than sweeping up the still-Deferred overshoot; that excess carries to the next run. Legacy runs + // (NULL target) flip all Deferred. Returns rows affected (0 if already confirmed or nothing found), so the caller can + // update `recruitment_run.candidates_found`. def confirmDeferredByRun(runId: RecruitmentRunId): ZIO[PostgresClient, SQLException, Int] = connectZIO { val invited: CandidateOutcome = CandidateOutcome.Invited val deferred: CandidateOutcome = CandidateOutcome.Deferred sql"""UPDATE recruitment_candidate SET outcome = $invited - WHERE run_id = $runId AND outcome = $deferred""".update.run() + WHERE run_id = $runId AND player_id IN ( + SELECT player_id FROM recruitment_candidate + WHERE run_id = $runId AND outcome = $deferred + ORDER BY player_id + LIMIT (SELECT CASE WHEN rr.target IS NULL THEN NULL + ELSE GREATEST(rr.target - (SELECT COUNT(*) FROM recruitment_candidate + WHERE run_id = $runId AND outcome = $invited), 0) + END + FROM recruitment_run rr WHERE rr.run_id = $runId) + ) AND outcome = $deferred""".update.run() } /** Returns deferred candidates for a club that have not been resolved (Invited/Rejected) in a later run. */ diff --git a/src/main/scala/ccas/analysis/tables/RecruitmentRun.scala b/src/main/scala/ccas/analysis/tables/RecruitmentRun.scala index 3a8e65e..14e408b 100644 --- a/src/main/scala/ccas/analysis/tables/RecruitmentRun.scala +++ b/src/main/scala/ccas/analysis/tables/RecruitmentRun.scala @@ -21,12 +21,16 @@ final case class RecruitmentRun( startedAt: Instant, completedAt: Option[Instant], candidatesFound: Int, + // The run's effective target (invite budget). Read by `selectDeferredByRun` / `confirmDeferredByRun` to cap the + // interactive confirm at target, leaving any chunk-overshoot excess Deferred to carry forward. NULL only for legacy + // rows created before this column existed (treated as "no cap"). + target: Option[Int], jobRunId: Option[JobRunId] ) derives DbCodec object RecruitmentRun { private val selectCols = SqlLiteral( - "run_id, club_id, criteria_id, trigger, started_at, completed_at, candidates_found, job_run_id" + "run_id, club_id, criteria_id, trigger, started_at, completed_at, candidates_found, target, job_run_id" ) def createTable: ZIO[PostgresClient, SQLException, Int] = @@ -39,6 +43,7 @@ object RecruitmentRun { started_at TIMESTAMPTZ NOT NULL, completed_at TIMESTAMPTZ, candidates_found INT NOT NULL, + target INT, job_run_id TEXT, FOREIGN KEY (club_id) REFERENCES club (club_id) ON DELETE RESTRICT, FOREIGN KEY (criteria_id) REFERENCES recruitment_criteria (criteria_id) ON DELETE RESTRICT @@ -82,11 +87,12 @@ object RecruitmentRun { criteriaId: Long, trigger: RunTrigger, startedAt: Instant, + target: Option[Int], jobRunId: Option[JobRunId] ): ZIO[PostgresClient, SQLException, RecruitmentRunId] = connectZIO { - sql"""INSERT INTO recruitment_run (club_id, criteria_id, trigger, started_at, candidates_found, job_run_id) - VALUES ($clubId, $criteriaId, $trigger, $startedAt, 0, $jobRunId) + sql"""INSERT INTO recruitment_run (club_id, criteria_id, trigger, started_at, candidates_found, target, job_run_id) + VALUES ($clubId, $criteriaId, $trigger, $startedAt, 0, $target, $jobRunId) RETURNING run_id""".query[RecruitmentRunId].run().headOption }.someOrFail(new SQLException("INSERT RETURNING produced no rows")) diff --git a/src/main/scala/ccas/cli/Dispatcher.scala b/src/main/scala/ccas/cli/Dispatcher.scala index 21d02ab..2bfb216 100644 --- a/src/main/scala/ccas/cli/Dispatcher.scala +++ b/src/main/scala/ccas/cli/Dispatcher.scala @@ -330,20 +330,69 @@ object Dispatcher { "they are listed in the job log above." ).orDie - // OSC 52 clipboard write: `ESC ] 52 ; c ; BEL`. The terminal itself owns the clipboard, so this - // works over SSH (tmux needs `set -g set-clipboard on`). Payloads are a handful of usernames, far under any - // terminal's OSC 52 size cap. ESC/BEL as \u escapes per the repo's no-raw-control-bytes convention. + // Copy the usernames to the system clipboard. A native clipboard tool is preferred: its forked daemon keeps owning + // the selection after this short-lived CLI exits, whereas an in-process JVM clipboard (AWT) would lose the contents + // the moment we exit. The OSC 52 terminal escape is only a fallback for a remote/SSH session whose *local* terminal + // implements it — many terminals (notably GNOME Terminal / VTE) silently ignore OSC 52 clipboard writes, which is + // why the old escape-only path copied nothing. The "Copied" line prints only when the copy actually succeeds. private def copyToClipboard(names: List[String]): Task[Unit] = if (names.isEmpty) { Console.printLineError("no invited usernames to copy").orDie } - // Callers only reach here on a TTY, but guard defensively: an OSC 52 escape to a non-terminal would corrupt the - // stream and no terminal is there to honour it, so fall back to just printing the usernames. - else if (!hasTty) { - Console.printLineError("clipboard unavailable (not an interactive terminal); printing usernames instead").orDie *> - printBare(names) - } else { + else { + clipboardCommand match { + case Some(cmd) => + copyViaTool(cmd, names).flatMap { copied => + if (copied) { Console.printLine(s"Copied ${names.size} usernames.").orDie } + else { copyFallback(names) } + } + case None => copyFallback(names) + } + } + + // The native clipboard command for this environment, if any: macOS -> pbcopy; Wayland -> wl-copy; X11 -> xclip. + // None on a headless / unknown session, which routes to the OSC 52 fallback. + private def clipboardCommand: Option[List[String]] = + if (isMac) { Some(List("pbcopy")) } + else if (envSet("WAYLAND_DISPLAY")) { Some(List("wl-copy")) } + else if (envSet("DISPLAY")) { Some(List("xclip", "-selection", "clipboard")) } + else { None } + + private def isMac: Boolean = sys.props.getOrElse("os.name", "").toLowerCase.contains("mac") + private def envSet(name: String): Boolean = Option(java.lang.System.getenv(name)).exists(_.nonEmpty) + + // Feed the newline-joined usernames to the clipboard tool's stdin, then wait. Returns true only if the process + // launched and exited 0; a launch failure (tool not installed -> IOException) or non-zero exit yields false so the + // caller falls back. Closing stdin sends EOF, which wl-copy / xclip / pbcopy wait for before taking ownership. + private def copyViaTool(cmd: List[String], names: List[String]): UIO[Boolean] = + ZIO.attemptBlocking { + val pb = new ProcessBuilder(cmd*) + // Discard the tool's stdout/stderr rather than leaving an un-drained pipe that could wedge waitFor if the tool + // ever got chatty (wl-copy / xclip / pbcopy are silent on success, but this is robust regardless). + pb.redirectOutput(ProcessBuilder.Redirect.DISCARD) + pb.redirectError(ProcessBuilder.Redirect.DISCARD) + val process = pb.start() + val stdin = process.getOutputStream + try { stdin.write(names.mkString("\n").getBytes(StandardCharsets.UTF_8)) } + finally { stdin.close() } + process.waitFor() == 0 + }.orElseSucceed(false) + + // No working native clipboard tool. The usernames were already printed by the caller (renderReport / confirmPrompt), + // so don't reprint them — on a TTY emit the OSC 52 escape as a best-effort copy (works only if the terminal honours + // it, e.g. an SSH session into kitty / tmux with `set -g set-clipboard on`) and note the caveat on stderr. ESC/BEL as + // \u escapes per the repo's no-raw-control-bytes convention. + private def copyFallback(names: List[String]): Task[Unit] = + if (hasTty) { val b64 = Base64.getEncoder.encodeToString(names.mkString("\n").getBytes(StandardCharsets.UTF_8)) val osc52 = "\u001b]52;c;" + b64 + "\u0007" - Console.print(osc52).orDie *> Console.printLine(s"Copied ${names.size} usernames.").orDie + Console.print(osc52).orDie *> + Console.printLineError( + "no clipboard tool found (install wl-copy or xclip); sent an OSC 52 copy escape as a fallback — it only " + + "works if your terminal supports it. The usernames are listed above." + ).orDie + } else { + Console.printLineError( + "clipboard unavailable (no clipboard tool and not an interactive terminal); the usernames are listed above." + ).orDie } private def parseMisfire(s: String): IO[CliError, MisfirePolicy] = diff --git a/src/test/scala/ccas/analysis/apps/recruitment/TestRecruitmentAppCore.scala b/src/test/scala/ccas/analysis/apps/recruitment/TestRecruitmentAppCore.scala index ac4fbbd..dd9b6b8 100644 --- a/src/test/scala/ccas/analysis/apps/recruitment/TestRecruitmentAppCore.scala +++ b/src/test/scala/ccas/analysis/apps/recruitment/TestRecruitmentAppCore.scala @@ -84,7 +84,7 @@ object TestRecruitmentAppCore extends ZIOSpecDefault { for { _ <- seedDb criteriaId <- seedCriteria(makeCriteria()) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) loaded <- RecruitmentRun.selectId(runId) } yield assertTrue( runId.value > 0L, @@ -99,9 +99,9 @@ object TestRecruitmentAppCore extends ZIOSpecDefault { for { _ <- seedDb criteriaId <- seedCriteria(makeCriteria()) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) _ <- RecruitmentRun.update( - RecruitmentRun(runId, clubId, criteriaId, RunTrigger.Cli, Times.t0, Some(Times.t1), 5, None) + RecruitmentRun(runId, clubId, criteriaId, RunTrigger.Cli, Times.t0, Some(Times.t1), 5, None, None) ) loaded <- RecruitmentRun.selectId(runId) } yield assertTrue( @@ -113,8 +113,8 @@ object TestRecruitmentAppCore extends ZIOSpecDefault { for { _ <- seedDb criteriaId <- seedCriteria(makeCriteria()) - _ <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) - runId2 <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t1, None) + _ <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) + runId2 <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t1, None, None) latest <- RecruitmentRun.selectLatest(clubId) } yield assertTrue( latest.isDefined, @@ -127,7 +127,7 @@ object TestRecruitmentAppCore extends ZIOSpecDefault { _ <- seedPlayer(pid0) _ <- seedPlayer(pid1) criteriaId <- seedCriteria(makeCriteria()) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) _ <- RecruitmentCandidate .insert( RecruitmentCandidate(runId, pid0, Times.t0, CandidateOutcome.Invited, None) @@ -145,7 +145,7 @@ object TestRecruitmentAppCore extends ZIOSpecDefault { _ <- seedPlayer(pid0) _ <- seedPlayer(pid1) criteriaId <- seedCriteria(makeCriteria()) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) _ <- RecruitmentCandidate .insert( RecruitmentCandidate(runId, pid0, Times.t0, CandidateOutcome.Invited, None) @@ -167,7 +167,7 @@ object TestRecruitmentAppCore extends ZIOSpecDefault { _ <- seedPlayer(pid1) _ <- seedPlayer(pid2) criteriaId <- seedCriteria(makeCriteria()) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) // Insert out of player_id order; the query must return them ascending regardless. _ <- ZIO.foreachDiscard(List(pid2, pid0, pid1)) { pid => RecruitmentCandidate.insert(RecruitmentCandidate(runId, pid, Times.t0, CandidateOutcome.Invited, None)) @@ -187,10 +187,10 @@ object TestRecruitmentAppCore extends ZIOSpecDefault { nowT <- dbNow earlier = nowT.minusSeconds(120) // Two completed runs today for the same club/alias. - runA <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, nowT, None) - _ <- RecruitmentRun.update(RecruitmentRun(runA, clubId, criteriaId, RunTrigger.Cli, nowT, Some(nowT), 0, None)) - runB <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, nowT, None) - _ <- RecruitmentRun.update(RecruitmentRun(runB, clubId, criteriaId, RunTrigger.Cli, nowT, Some(nowT), 0, None)) + runA <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, nowT, None, None) + _ <- RecruitmentRun.update(RecruitmentRun(runA, clubId, criteriaId, RunTrigger.Cli, nowT, Some(nowT), 0, None, None)) + runB <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, nowT, None, None) + _ <- RecruitmentRun.update(RecruitmentRun(runB, clubId, criteriaId, RunTrigger.Cli, nowT, Some(nowT), 0, None, None)) // pid0 invited in BOTH runs (the duplicate); pid1 only in A; pid2 only in B. _ <- RecruitmentCandidate.insert(RecruitmentCandidate(runA, pid0, earlier, CandidateOutcome.Invited, None)) _ <- RecruitmentCandidate.insert(RecruitmentCandidate(runB, pid0, nowT, CandidateOutcome.Invited, None)) @@ -212,7 +212,7 @@ object TestRecruitmentAppCore extends ZIOSpecDefault { _ <- ZIO.foreachDiscard(enumPids)(seedPlayer) outcomes = CandidateOutcome.values.toList criteriaId <- seedCriteria(makeCriteria()) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) _ <- ZIO.foreachDiscard(outcomes.zip(enumPids)) { (outcome, pid) => RecruitmentCandidate .insert( @@ -231,8 +231,8 @@ object TestRecruitmentAppCore extends ZIOSpecDefault { _ <- seedDb _ <- seedPlayer(pid0) criteriaId <- seedCriteria(makeCriteria()) - runId1 <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) - runId2 <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t1, None) + runId1 <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) + runId2 <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t1, None, None) _ <- RecruitmentCandidate .insert( RecruitmentCandidate(runId1, pid0, Times.t0, CandidateOutcome.Invited, None) @@ -254,7 +254,7 @@ object TestRecruitmentAppCore extends ZIOSpecDefault { _ <- Club.upsert(otherClub) _ <- seedPlayer(pid0) criteriaId <- seedCriteria(makeCriteria()) - otherRunId <- RecruitmentRun.insert(sourceClubId, criteriaId, RunTrigger.Cli, Times.t0, None) + otherRunId <- RecruitmentRun.insert(sourceClubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) _ <- RecruitmentCandidate.insert( RecruitmentCandidate(otherRunId, pid0, Times.t0, CandidateOutcome.Invited, None) ) @@ -417,9 +417,9 @@ object TestRecruitmentAppCore extends ZIOSpecDefault { _ <- seedPlayer(pid0) _ <- seedPlayer(pid1) criteriaId <- seedCriteria(makeCriteria()) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) _ <- RecruitmentRun.update( - RecruitmentRun(runId, clubId, criteriaId, RunTrigger.Cli, Times.t0, Some(Times.t1), 2, None) + RecruitmentRun(runId, clubId, criteriaId, RunTrigger.Cli, Times.t0, Some(Times.t1), 2, None, None) ) _ <- RecruitmentCandidate .insert( @@ -437,9 +437,9 @@ object TestRecruitmentAppCore extends ZIOSpecDefault { _ <- seedDb _ <- seedPlayer(pid0) criteriaId <- seedCriteria(makeCriteria()) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) _ <- RecruitmentRun.update( - RecruitmentRun(runId, clubId, criteriaId, RunTrigger.Cli, Times.t0, Some(Times.t1), 1, None) + RecruitmentRun(runId, clubId, criteriaId, RunTrigger.Cli, Times.t0, Some(Times.t1), 1, None, None) ) _ <- RecruitmentCandidate .insert( @@ -471,7 +471,7 @@ object TestRecruitmentAppCore extends ZIOSpecDefault { _ <- seedPlayer(PlayerId(999)) _ <- seedMatchWithBoard(refMatchId, Some(clubId), candidatePid, PlayerId(999)) client <- fakeChessComClient(responses) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Instant.now(), None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Instant.now(), None, None) _ <- evalCandidates(client, runId, List(Username("ref-db-player")), criteria) ref <- PlayerMatchRef.selectId(candidatePid) } yield assertTrue( @@ -504,7 +504,7 @@ object TestRecruitmentAppCore extends ZIOSpecDefault { _ <- seedDb criteriaId <- seedCriteria(criteria) client <- fakeChessComClient(responses) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Instant.now(), None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Instant.now(), None, None) _ <- evalCandidates(client, runId, List(Username("ref-api-player")), criteria) ref <- PlayerMatchRef.selectId(candidatePid) } yield assertTrue( @@ -560,7 +560,7 @@ object TestRecruitmentAppCore extends ZIOSpecDefault { _ <- seedDb criteriaId <- seedCriteria(criteria) client <- fakeChessComClient(responses) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Instant.now(), None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Instant.now(), None, None) invited <- evalCandidates(client, runId, List(Username("ref-fail-player")), criteria) ref <- PlayerMatchRef.selectId(candidatePid) } yield assertTrue( diff --git a/src/test/scala/ccas/analysis/apps/recruitment/TestRecruitmentAppExplore.scala b/src/test/scala/ccas/analysis/apps/recruitment/TestRecruitmentAppExplore.scala index 8b2cd9e..d44ef69 100644 --- a/src/test/scala/ccas/analysis/apps/recruitment/TestRecruitmentAppExplore.scala +++ b/src/test/scala/ccas/analysis/apps/recruitment/TestRecruitmentAppExplore.scala @@ -165,9 +165,10 @@ object TestRecruitmentAppExplore extends ZIOSpecDefault { deferred.nonEmpty ) } @@ TestAspect.withLiveClock, - test("excess invited candidates are reclassified as Deferred") { + test("excess invited candidates stay Deferred and carry forward (auto-confirm)") { val source = ClubSlug("defer-source") - // 6 candidates, target=2 → should get exactly 2 Invited, rest Deferred or Rejected + // 6 candidates, target=2 → exactly 2 Invited; the 4 chunk-overshoot passers stay Deferred (NOT deleted) so they + // carry to the next run via selectDeferredByClub. val candidateNames = (0 to 5).map(i => s"defer-cand-$i").toList val responses = Map( s"club/$clubSlug" -> apiClubJson(clubId.value, clubSlug.value), @@ -192,7 +193,51 @@ object TestRecruitmentAppExplore extends ZIOSpecDefault { } yield assertTrue( invited.size == 2, result.candidatesFound == 2, - invited.size + deferred.size >= 2 + deferred.size == candidateNames.size - 2 + ) + }, + test("deferred-confirm caps invited at target, leaving excess Deferred for next run") { + // Regression for the 31-vs-30 bug: a chunk overshoot persists more Deferred than target, and the old uncapped + // confirm flipped ALL of them. Now /found (selectDeferredByRun) and the confirm both cap at the run's stored + // target, and the excess stays Deferred to carry forward. + val source = ClubSlug("confirm-source") + val candidateNames = (0 to 5).map(i => s"confirm-cand-$i").toList + val responses = Map( + s"club/$clubSlug" -> apiClubJson(clubId.value, clubSlug.value), + s"club/$clubSlug/members" -> apiClubMembersJson(Nil), + s"club/$source" -> apiClubJson(ClubId(902).value, source.value), + s"club/$source/members" -> apiClubMembersJson( + candidateNames.map(n => (n, Times.t0.getEpochSecond)) + ) + ) ++ candidateNames.zipWithIndex.map { (name, i) => + s"player/$name" -> apiPlayerJson(410 + i, name) + }.toMap + val criteria = makeCriteria() + + for { + _ <- seedDb + _ <- seedCriteria(criteria) + client <- fakeChessComClient(responses) + result <- runRecruit(client, target = Some(2), sourceClubs = List(source), autoConfirm = false) + allBefore <- RecruitmentCandidate.selectByRun(result.runId) + allDeferredBefore = allBefore.filter(_.outcome == CandidateOutcome.Deferred) + foundBefore <- RecruitmentCandidate.selectDeferredByRun(result.runId) + firstFlip <- RecruitmentCandidate.confirmDeferredByRun(result.runId) + invited <- RecruitmentCandidate.selectInvitedByRun(result.runId) + // Re-POST must be idempotent (JobRoutes contract): flip 0, invited stays at target — it must NOT sweep up the + // still-Deferred overshoot. + secondFlip <- RecruitmentCandidate.confirmDeferredByRun(result.runId) + invitedAfterRepost <- RecruitmentCandidate.selectInvitedByRun(result.runId) + allAfter <- RecruitmentCandidate.selectByRun(result.runId) + deferredAfter = allAfter.filter(_.outcome == CandidateOutcome.Deferred) + } yield assertTrue( + allDeferredBefore.size == candidateNames.size, // all 6 evaluated, nothing deleted + foundBefore.size == 2, // /found capped at target + firstFlip == 2, // confirm flips exactly target + invited.size == 2, // confirm capped at target + secondFlip == 0, // re-POST flips nothing (idempotent) + invitedAfterRepost.size == 2, // still exactly target after re-POST + deferredAfter.size == candidateNames.size - 2 // excess (4) carried forward as Deferred ) }, test("deferred candidates from prior run are prioritised in next run") { @@ -216,9 +261,9 @@ object TestRecruitmentAppExplore extends ZIOSpecDefault { client <- fakeChessComClient(responses) // Seed prior run with a Deferred candidate (need Player row) _ <- Player.insert(Player(PlayerId(500), Times.t0, Username("prio-deferred"), Active, None, Times.t0)) - priorRunId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + priorRunId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) _ <- RecruitmentRun.update( - RecruitmentRun(priorRunId, clubId, criteriaId, RunTrigger.Cli, Times.t0, Some(Times.t1), 0, None) + RecruitmentRun(priorRunId, clubId, criteriaId, RunTrigger.Cli, Times.t0, Some(Times.t1), 0, None, None) ) _ <- RecruitmentCandidate.insert( RecruitmentCandidate(priorRunId, PlayerId(500), Times.t0, CandidateOutcome.Deferred, None) @@ -253,9 +298,9 @@ object TestRecruitmentAppExplore extends ZIOSpecDefault { _ <- Player.insert(Player(PlayerId(600), Times.t0, Username("resolved-player"), Active, None, Times.t0)) // Run 1: candidate is Deferred - runId1 <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId1 <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) _ <- RecruitmentRun.update( - RecruitmentRun(runId1, clubId, criteriaId, RunTrigger.Cli, Times.t0, Some(Times.t1), 0, None) + RecruitmentRun(runId1, clubId, criteriaId, RunTrigger.Cli, Times.t0, Some(Times.t1), 0, None, None) ) _ <- RecruitmentCandidate.insert( RecruitmentCandidate(runId1, PlayerId(600), Times.t0, CandidateOutcome.Deferred, None) @@ -265,9 +310,9 @@ object TestRecruitmentAppExplore extends ZIOSpecDefault { deferredBefore <- RecruitmentCandidate.selectDeferredByClub(clubId) // Run 2: same candidate is Invited (later timestamp) - runId2 <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t2, None) + runId2 <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t2, None, None) _ <- RecruitmentRun.update( - RecruitmentRun(runId2, clubId, criteriaId, RunTrigger.Cli, Times.t2, Some(Times.t3), 1, None) + RecruitmentRun(runId2, clubId, criteriaId, RunTrigger.Cli, Times.t2, Some(Times.t3), 1, None, None) ) _ <- RecruitmentCandidate.insert( RecruitmentCandidate(runId2, PlayerId(600), Times.t2, CandidateOutcome.Invited, None) diff --git a/src/test/scala/ccas/analysis/apps/recruitment/TestRecruitmentAppNetworkOutage.scala b/src/test/scala/ccas/analysis/apps/recruitment/TestRecruitmentAppNetworkOutage.scala index b2d33e2..6caf239 100644 --- a/src/test/scala/ccas/analysis/apps/recruitment/TestRecruitmentAppNetworkOutage.scala +++ b/src/test/scala/ccas/analysis/apps/recruitment/TestRecruitmentAppNetworkOutage.scala @@ -36,7 +36,7 @@ object TestRecruitmentAppNetworkOutage extends ZIOSpecDefault { for { _ <- seedDb criteriaId <- seedCriteria(criteria) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) client <- TestChessComClientSupport.networkDownClient discovered <- Ref.make(Set.empty[Username]) failed <- Ref.make(Set.empty[ClubSlug]) diff --git a/src/test/scala/ccas/analysis/apps/recruitment/TestRecruitmentBlacklist.scala b/src/test/scala/ccas/analysis/apps/recruitment/TestRecruitmentBlacklist.scala index 2d7b2fb..24e1e27 100644 --- a/src/test/scala/ccas/analysis/apps/recruitment/TestRecruitmentBlacklist.scala +++ b/src/test/scala/ccas/analysis/apps/recruitment/TestRecruitmentBlacklist.scala @@ -45,7 +45,7 @@ object TestRecruitmentBlacklist extends ZIOSpecDefault { _ <- RecruitmentBlacklist.insert( RecruitmentBlacklist(clubId, pid0, Times.t0, expiresAt = None, reason = Some("banned")) ) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) client <- fakeChessComClient(responses) _ <- evalCandidates(client, runId, List(Username("alice")), criteria) cands <- RecruitmentCandidate.selectByRun(runId) @@ -73,7 +73,7 @@ object TestRecruitmentBlacklist extends ZIOSpecDefault { reason = Some("temp ban") ) ) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, now, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, now, None, None) client <- fakeChessComClient(responses) _ <- evalCandidates(client, runId, List(Username("alice")), criteria) cands <- RecruitmentCandidate.selectByRun(runId) diff --git a/src/test/scala/ccas/analysis/apps/recruitment/TestRecruitmentEvaluation.scala b/src/test/scala/ccas/analysis/apps/recruitment/TestRecruitmentEvaluation.scala index 8633f57..f2cbe99 100644 --- a/src/test/scala/ccas/analysis/apps/recruitment/TestRecruitmentEvaluation.scala +++ b/src/test/scala/ccas/analysis/apps/recruitment/TestRecruitmentEvaluation.scala @@ -43,7 +43,7 @@ object TestRecruitmentEvaluation extends ZIOSpecDefault { for { _ <- seedDb criteriaId <- seedCriteria(criteria) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) client <- fakeChessComClient(responses) invited <- evalCandidates(client, runId, List(Username("alice"), Username("bob")), criteria) // Check invited list @@ -74,7 +74,7 @@ object TestRecruitmentEvaluation extends ZIOSpecDefault { for { _ <- seedDb criteriaId <- seedCriteria(criteria) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) client <- fakeChessComClient(responses) invited <- evalCandidates( client, @@ -99,7 +99,7 @@ object TestRecruitmentEvaluation extends ZIOSpecDefault { for { _ <- seedDb criteriaId <- seedCriteria(criteria) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) client <- fakeChessComClient(responses, failures = Set("bob")) invited <- evalCandidates(client, runId, List(Username("alice"), Username("bob")), criteria) candidates <- RecruitmentCandidate.selectByRun(runId) @@ -122,7 +122,7 @@ object TestRecruitmentEvaluation extends ZIOSpecDefault { for { _ <- seedDb criteriaId <- seedCriteria(criteria) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) client <- fakeChessComClient(responses) _ <- evalCandidates(client, runId, List(Username("alice")), criteria) candidates <- RecruitmentCandidate.selectByRun(runId) @@ -152,7 +152,7 @@ object TestRecruitmentEvaluation extends ZIOSpecDefault { _ <- seedPlayer(cache.playerId) _ <- PlayerRecruitmentCache.upsert(cache) criteriaId <- seedCriteria(criteria) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) client <- fakeChessComClient(responses) _ <- evalCandidates(client, runId, List(Username.wrap(username)), criteria) cands <- RecruitmentCandidate.selectByRun(runId) diff --git a/src/test/scala/ccas/analysis/apps/recruitment/TestRecruitmentFilterChain.scala b/src/test/scala/ccas/analysis/apps/recruitment/TestRecruitmentFilterChain.scala index d8c9c65..8fb3d7d 100644 --- a/src/test/scala/ccas/analysis/apps/recruitment/TestRecruitmentFilterChain.scala +++ b/src/test/scala/ccas/analysis/apps/recruitment/TestRecruitmentFilterChain.scala @@ -37,7 +37,7 @@ object TestRecruitmentFilterChain extends ZIOSpecDefault { for { _ <- seedDb criteriaId <- seedCriteria(criteria) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) client <- fakeChessComClient(responses) invited <- evalCandidates(client, runId, List(Username.wrap(username)), criteria) } yield @@ -233,7 +233,7 @@ object TestRecruitmentFilterChain extends ZIOSpecDefault { for { _ <- seedDb criteriaId <- seedCriteria(criteria) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) client <- fakeChessComClient(responses) _ <- evalCandidates(client, runId, List(Username.wrap("alice")), criteria) failures <- ApiFetchFailure.selectRecent(now.minus(Duration.ofMinutes(1))) @@ -273,7 +273,7 @@ object TestRecruitmentFilterChain extends ZIOSpecDefault { for { _ <- seedDb criteriaId <- seedCriteria(criteria) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) client <- fakeChessComClient(responses) _ <- evalCandidates(client, runId, List(Username.wrap("alice")), criteria) cache <- PlayerRecruitmentCache.selectId(pid0) @@ -321,7 +321,7 @@ object TestRecruitmentFilterChain extends ZIOSpecDefault { _ <- seedDb _ <- Club.upsert(Club(bannedClubId, Times.t0, ClubSlug("banned-club"), "Banned Club", None, None, None)) criteriaId <- seedCriteria(criteria) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) client <- fakeChessComClient(responses) _ <- evalCandidates(client, runId, List(Username.wrap("alice")), criteria) cands <- RecruitmentCandidate.selectByRun(runId) @@ -355,12 +355,12 @@ object TestRecruitmentFilterChain extends ZIOSpecDefault { // Seed player row for FK constraint _ <- seedPlayer(pid0) // Create a prior run with alice invited recently - priorRunId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Instant.now(), None) + priorRunId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Instant.now(), None, None) _ <- RecruitmentCandidate.insert( RecruitmentCandidate(priorRunId, pid0, Instant.now(), CandidateOutcome.Invited, None) ) // Now evaluate alice again - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) client <- fakeChessComClient(Map("player/alice" -> apiPlayerJson(200, "alice"))) _ <- evalCandidates(client, runId, List(Username("alice")), criteria) cands <- RecruitmentCandidate.selectByRun(runId) @@ -451,7 +451,7 @@ object TestRecruitmentFilterChain extends ZIOSpecDefault { for { _ <- seedDb criteriaId <- seedCriteria(criteria) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) client <- fakeChessComClient(responses) _ <- evalCandidates(client, runId, List(Username("alice")), criteria) cached <- PlayerRecruitmentCache.selectId(pid0) @@ -474,7 +474,7 @@ object TestRecruitmentFilterChain extends ZIOSpecDefault { // Seed alice as a former member of the club (player row needed for FK) _ <- seedPlayer(pid0) _ <- ClubMember.insert(ClubMember(clubId, pid0, Times.t0, Some(Times.t1), sinceApproximate = false)) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) client <- fakeChessComClient(responses) _ <- evalCandidates(client, runId, List(Username("alice")), criteria) cands <- RecruitmentCandidate.selectByRun(runId) @@ -490,7 +490,7 @@ object TestRecruitmentFilterChain extends ZIOSpecDefault { // Seed alice as a former member of the club _ <- seedPlayer(pid0) _ <- ClubMember.insert(ClubMember(clubId, pid0, Times.t0, Some(Times.t1), sinceApproximate = false)) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) client <- fakeChessComClient(responses) _ <- evalCandidates(client, runId, List(Username("alice")), criteria) cands <- RecruitmentCandidate.selectByRun(runId) @@ -507,7 +507,7 @@ object TestRecruitmentFilterChain extends ZIOSpecDefault { _ <- seedPlayer(pid0) _ <- ClubAdmin.insertBatch(List(ClubAdmin(sizableClubId, pid0))) criteriaId <- seedCriteria(criteria) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) client <- fakeChessComClient(responses) _ <- evalCandidates(client, runId, List(Username("alice")), criteria) cands <- RecruitmentCandidate.selectByRun(runId) @@ -524,7 +524,7 @@ object TestRecruitmentFilterChain extends ZIOSpecDefault { _ <- seedPlayer(pid0) _ <- ClubAdmin.insertBatch(List(ClubAdmin(sizableClubId, pid0))) criteriaId <- seedCriteria(criteria) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) client <- fakeChessComClient(responses) _ <- evalCandidates(client, runId, List(Username("alice")), criteria) cands <- RecruitmentCandidate.selectByRun(runId) @@ -548,7 +548,7 @@ object TestRecruitmentFilterChain extends ZIOSpecDefault { for { _ <- seedDb criteriaId <- seedCriteria(criteria) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) client <- fakeChessComClient(responses) _ <- evalCandidates(client, runId, List(Username("alice")), criteria) cands <- RecruitmentCandidate.selectByRun(runId) @@ -582,7 +582,7 @@ object TestRecruitmentFilterChain extends ZIOSpecDefault { for { _ <- seedDb criteriaId <- seedCriteria(criteria) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) client <- fakeChessComClient(responses) _ <- evalCandidates(client, runId, List(Username("alice"), Username("bob")), criteria) cands <- RecruitmentCandidate.selectByRun(runId) @@ -609,7 +609,7 @@ object TestRecruitmentFilterChain extends ZIOSpecDefault { for { _ <- seedDb criteriaId <- seedCriteria(criteria) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Cli, Times.t0, None, None) client <- fakeChessComClient(responses) _ <- evalCandidates(client, runId, List(Username("alice")), criteria) cands <- RecruitmentCandidate.selectByRun(runId) diff --git a/src/test/scala/ccas/server/routes/TestRoutes.scala b/src/test/scala/ccas/server/routes/TestRoutes.scala index e3c89ec..8823a73 100644 --- a/src/test/scala/ccas/server/routes/TestRoutes.scala +++ b/src/test/scala/ccas/server/routes/TestRoutes.scala @@ -487,7 +487,7 @@ object TestRoutes extends ZIOSpecDefault { for { _ <- ensureClubs criteriaId <- RecruitmentCriteria.insert(RecruitmentCriteria.defaultDaily) - runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Api, t0, Some(JobRunId.wrap(jobId))) + runId <- RecruitmentRun.insert(clubId, criteriaId, RunTrigger.Api, t0, None, Some(JobRunId.wrap(jobId))) _ <- ZIO.foreachDiscard(invited ++ deferred) { case (pid, name) => seedPlayer(pid, name) } _ <- ZIO.foreachDiscard(invited) { case (pid, _) => RecruitmentCandidate.insert(RecruitmentCandidate(runId, PlayerId(pid), t0, CandidateOutcome.Invited, None))