Skip to content

refactor(cue): harden config boundary, introduce session registry, and isolate trigger sources#779

Merged
reachrazamair merged 5 commits intorcfrom
cue-polish
Apr 10, 2026
Merged

refactor(cue): harden config boundary, introduce session registry, and isolate trigger sources#779
reachrazamair merged 5 commits intorcfrom
cue-polish

Conversation

@reachrazamair
Copy link
Copy Markdown
Contributor

@reachrazamair reachrazamair commented Apr 9, 2026

Summary

This PR closes out the Maestro Cue refactor across three sequential commits, eliminating the root causes of the recent fix(cue): cascade and bringing the engine to a structurally sound baseline.

What was wrong

The previous architecture had three concrete failure modes:

  1. No config boundaryprompt_file/output_prompt_file leaked past normalization into the runtime model; the executor contained a live file-resolver that re-read .md/.txt/.yaml files at trigger time instead of using the already-resolved inline content; IPC handlers owned raw fs/path I/O directly; loadCueConfig() returned null for both "file missing" and "file invalid" with no distinction.

  2. No session state ownerscheduledFiredKeys and startupFiredKeys were two bare Set instances shared by reference across CueEngine, CueSessionRuntimeService, and cue-subscription-setup.ts. Any of the three could mutate them. The isBootScan boolean was a closure flag threaded through the engine start() signature as a boolean, causing the recent fix(cue): harden watcher, fan-out… and fix(cue): tighten validator… cascades.

  3. No trigger source abstraction — all trigger types (heartbeat, scheduled, file watcher, task scanner, GitHub poller) were wired by cue-subscription-setup.ts via parallel timers[], watchers[], and nextTriggers Map arrays on SessionState. Filter checks were duplicated across each arm of the if/else chain. Teardown had to know the internal shape of each trigger type.


Commit 1 — refactor(cue): harden config boundary and validation

Config normalizer is now authoritative.

  • materializeCueConfig() strips prompt_file and output_prompt_file before they reach the runtime model. The executor never reads files; it receives inline content or fails fast with a clear error.
  • loadCueConfigDetailed() returns a discriminated union { ok: true, config, warnings } | { ok: false, reason: 'missing' | 'parse-error' | 'invalid', … }. The session runtime service surfaces distinct log messages for each failure case.
  • cue-config-repository.ts becomes the single owner of all .maestro/cue.yaml and .maestro/prompts/*.md filesystem I/O (writeCueConfigFile, deleteCueConfigFile, writeCuePromptFile). IPC handlers in cue.ts are reduced to thin delegates.
  • pipeline-layout-store.ts (new) owns userData/cue-pipeline-layout.json; app.getPath('userData') is no longer called inline in the IPC handler.
  • prompt_file? and output_prompt_file? removed from CueSubscription in contracts.ts.

Files changed: cue-executor.ts, cue-dispatch-service.ts, cue-yaml-loader.ts, cue-session-runtime-service.ts, config/cue-config-normalizer.ts, config/cue-config-repository.ts, ipc/handlers/cue.ts, pipeline-layout-store.ts (new), shared/cue/contracts.ts

Tests added/updated: cue-config-repository.test.ts (17 new), cue-executor.test.ts, cue-yaml-loader.test.ts, cue-ipc-handlers.test.ts, 9 files updated for loadCueConfigDetailed mock shape.


Commit 2 — refactor(cue): introduce session registry and recovery service

Session state has a single owner.

  • CueSessionRegistry (new) owns the sessions map, scheduledFiredKeys, and startupFiredKeys behind typed accessor methods (markScheduledFired, evictStaleScheduledKeys, markStartupFired, clearStartupForSession, clearScheduledForSession). No external code touches the underlying Sets.
  • CueRecoveryService (new) owns DB init/prune, sleep detection, and reconciliation dispatch — previously split between cue-heartbeat.ts and cue-engine.ts.
  • CueEngine.start() signature changes from start(isBootScan: boolean) to start(reason: SessionInitReason) where SessionInitReason = 'system-boot' | 'user-toggle' | 'refresh' | 'discovery'. app.startup gating in CueSessionRuntimeService.initSession() checks opts.reason === 'system-boot' instead of a closure flag.
  • cue-heartbeat.ts is trimmed to the 30-second writer loop only.
  • src/main/index.ts updated: cueEngine.start(true)cueEngine.start('system-boot').

Files changed: cue-engine.ts, cue-session-runtime-service.ts, cue-heartbeat.ts, cue-session-state.ts, cue-session-registry.ts (new), cue-recovery-service.ts (new), src/main/index.ts

Tests added/updated: cue-session-registry.test.ts (15 new), cue-recovery-service.test.ts (10 new), cue-session-lifecycle.test.ts (+7 init-reason matrix cases), cue-startup.test.ts, cue-concurrency.test.ts, cue-engine.test.ts.


Commit 3 — refactor(cue): isolate trigger sources behind a registry

Every trigger type is a first-class object.

  • src/main/cue/triggers/ (new directory) contains:
    • cue-trigger-source.tsCueTriggerSource interface (start, stop, nextTriggerAt) + CueTriggerSourceContext
    • cue-trigger-filter.tspassesFilter(sub, event, onLog): boolean, the single filter call site; eliminates 6 duplicated filter blocks
    • cue-schedule-utils.tscalculateNextScheduledTime + getDayName (moved from cue-subscription-setup.ts)
    • cue-trigger-source-registry.tscreateTriggerSource(eventType, ctx) factory
    • Five source implementations: cue-heartbeat-trigger-source.ts, cue-scheduled-trigger-source.ts, cue-file-watcher-trigger-source.ts, cue-task-scanner-trigger-source.ts, cue-github-poller-trigger-source.ts
  • SessionState.timers[] + SessionState.watchers[] + SessionState.nextTriggers Map replaced by SessionState.triggerSources: CueTriggerSource[]. Teardown is a single source.stop() loop.
  • CueSessionRuntimeService subscription setup loop replaces the if/else chain with createTriggerSource(sub.event, ctx).
  • cue-subscription-setup.ts deleted.

Files changed: cue-session-runtime-service.ts, cue-session-state.ts, triggers/ (9 new files), cue-subscription-setup.ts (deleted)

Tests added: 56 new tests across 7 files in src/__tests__/main/cue/triggers/ covering dedup semantics, filter routing, timer lifecycle, registry factory, and per-source start/stop idempotency.


Test plan

  • npm run lint — zero type errors
  • npm run lint:eslint — zero lint errors
  • npm run test — full suite green (24,787 passing, up from 24,699 baseline)
  • npm run test -- src/__tests__/main/cue/ — all Cue-specific tests pass

Summary by CodeRabbit

  • New Features

    • Richer Cue config loading with validation/warnings and pipeline layout save/load.
    • New trigger sources for heartbeat, scheduled, file, GitHub and task scanner events.
  • Improvements

    • Prompts are materialized at config-load for predictable dispatch and safer prompt file handling.
    • Engine startup now distinguishes system boot vs user toggle to avoid duplicate startup runs.
    • Centralized session registry and improved sleep/wake recovery to reconcile missed events.
  • Bug Fixes

    • Empty or unresolved prompts now skip dispatch with clear warnings.

Closes the Phase 2 loopholes left after the initial config-boundary split:

- Delete the executor's prompt-file resolver. The normalizer already reads
  prompt files at config-load time, so the executor's `endsWith('.md')` /
  `fs.readFileSync` branch was dead code AND a footgun for inline prompts
  ending in a recognized extension. The executor now treats `promptPath` as
  inline content unconditionally and fails fast on empty content.

- Drop the dead `?? sub.prompt_file ?? ''` fallback in subscription-setup
  and dispatch-service. Post-normalization `sub.prompt` is always a string,
  so the fallback never fired and only obscured the contract.

- Remove `prompt_file` and `output_prompt_file` from the shared
  CueSubscription contract. These fields were stripped of meaning after
  materialization but kept leaking out as orphan metadata. They now live
  only on the internal CueSubscriptionDocument used by the normalizer.

- Add `loadCueConfigDetailed()` returning a structured result that
  distinguishes missing / parse-error / invalid / valid configs and carries
  non-fatal warnings. The session runtime service migrates to this entry
  point so the engine logs *why* a config failed (not just "missing") and
  surfaces unresolved prompt_file references via the warnings array.

- Move the prompt-file-missing warning out of session-runtime-service and
  into the materializer where it belongs, then re-emit through the loader's
  warnings array. Single owner, single source.

- Push `cue:readYaml` / `cue:writeYaml` / `cue:deleteYaml` IPC handlers
  through cue-config-repository. Add `writeCueConfigFile`,
  `deleteCueConfigFile`, and `writeCuePromptFile` so the repository owns
  every fs/path/mkdir for `.maestro/cue.yaml` and `.maestro/prompts/`.

- Extract pipeline-layout-store as the single owner of the
  `cue-pipeline-layout.json` file under userData. The IPC handler is now
  a thin transport with no direct fs/path imports.

Tests:

- 17 new cue-config-repository unit tests covering canonical-vs-legacy
  fallback, write/delete/promptFile semantics, and directory creation.
- 8 new loadCueConfigDetailed tests covering parse-error, invalid, and
  warning paths.
- Updated 8 cue test files to expose `loadCueConfigDetailed` on their
  yaml-loader mock (auto-derived from the existing `loadCueConfig` mock).
- Rewrote cue-ipc-handlers tests to mock the repository module directly,
  matching the new thin-transport shape.
- Updated cue-executor tests to assert the executor never touches the
  filesystem for prompt resolution.

Cue suite: 584 passing (up from 556). Full repo: 24,699 passing.
Closes the Phase 3 loopholes left after the initial engine decomposition.
The critical fix is L6: session state and dedup keys now have a single owner.

CueSessionRegistry (new)
- Sole owner of `Map<sessionId, SessionState>` plus the `time.scheduled` and
  `app.startup` dedup key sets that previously lived on CueEngine and were
  mutated by three different files (engine, runtime service, subscription
  setup).
- Public surface forces all dedup mutation through typed methods:
  markScheduledFired/evictStaleScheduledKeys/clearScheduledForSession and
  markStartupFired/clearStartupForSession.
- `clear()` matches the old engine.stop() semantics: drops sessions and
  scheduled keys but PRESERVES startup keys, so toggling Cue off/on does
  not re-fire app.startup subscriptions. This is the regression target.

CueRecoveryService (new)
- Wraps three previously-scattered concerns: DB init+prune at engine start,
  sleep-gap detection, and missed-event reconciliation. Engine no longer
  imports cue-db directly.
- cue-heartbeat.ts shrinks to just the 30s writer loop. The
  detectSleepAndReconcile logic moved to the recovery service so engine and
  bootstrap each call a single method.

CueEngine slimming
- Removes the private scheduledFiredKeys/startupFiredKeys/isBootScan fields.
  All session state lives behind the registry.
- start() takes a SessionInitReason instead of an isSystemBoot boolean.
  start('system-boot') fires app.startup; start() / start('user-toggle')
  does not. The intent now lives in the call signature, not a closure flag.
- All getSessionStates() / getSessionConfigs() callsites read from the
  registry directly; CueSessionRuntimeService no longer exposes those.
- Bootstrap is now: recoveryService.init() → registry-driven session loop →
  recoveryService.detectSleepAndReconcile() → heartbeat.start().

CueSessionRuntimeService
- initSession takes an explicit `{ reason }` parameter.
- All dedup mutation goes through the registry. The function no longer
  receives Set instances by reference.
- refreshSession passes reason='refresh' (so YAML hot-reload doesn't re-fire
  app.startup). useCueAutoDiscovery callers will pass 'discovery' next.

cue-subscription-setup.ts
- setupScheduledSubscription now uses registry.markScheduledFired and
  registry.evictStaleScheduledKeys instead of mutating a shared Set. This
  module is being deprecated entirely in Commit 3 (Phase 4) but compiles
  cleanly against the registry interface in the meantime.

L9 — triggerSubscription now reads the registry directly instead of
  calling sessionRuntimeService.getSessionStates().

L10 — cue-engine.ts no longer re-exports calculateNextScheduledTime for
  test compat. cue-engine.test.ts imports it directly from the real home
  in cue-subscription-setup.

Tests
- 15 new CueSessionRegistry unit tests covering register/snapshot,
  scheduled dedup (mark/evict/clear), startup dedup, and the clear()
  preservation policy that protects toggle-on/off behaviour.
- 10 new CueRecoveryService unit tests covering DB init success/failure,
  sleep-gap thresholds, reconciliation dispatch, and shutdown safety.
- 7 new init-reason matrix tests in cue-session-lifecycle.test.ts covering
  every entry to initSession (system-boot, user-toggle, refresh, removal).
- Updated existing engine.start(true) callsites to engine.start('system-boot')
  across cue-startup.test.ts (19 tests) and cue-concurrency.test.ts.

Cue suite: 616 passing (up from 584). Full repo: 24,731 passing.
Cue refactor: each trigger type becomes a first-class source
object that owns its own underlying mechanism (interval, watcher, poller).
The cue-subscription-setup.ts orchestration hub is deleted.

New module: src/main/cue/triggers/

  cue-trigger-source.ts            interface + context contract
  cue-trigger-filter.ts            centralized passesFilter helper (closes L12)
  cue-schedule-utils.ts            calculateNextScheduledTime (was in setup)
  cue-trigger-source-registry.ts   factory: event type → source
  cue-heartbeat-trigger-source.ts  setInterval-backed
  cue-scheduled-trigger-source.ts  per-minute polling + registry dedup
  cue-file-watcher-trigger-source.ts   wraps cue-file-watcher.ts
  cue-task-scanner-trigger-source.ts   wraps cue-task-scanner.ts
  cue-github-poller-trigger-source.ts  wraps cue-github-poller.ts

Each source exposes start() / stop() / nextTriggerAt() and emits events
through a context-supplied `emit` callback. The runtime never has to know
whether a source owns timers, watchers, or pollers — teardown is just
`source.stop()` for each entry.

L12 — the `if (sub.filter && !matchesFilter(...))` check that previously
  appeared 6+ times now lives in `passesFilter`. Every trigger source calls
  it exactly once before invoking `emit`.

L13 — the heartbeat fire-immediately path no longer duplicates the on-interval
  path; both go through one `fire()` helper inside the heartbeat source.

L14 — `dispatchOrExecuteSubscription` (which duplicated CueDispatchService's
  internal fan-out check) is gone. Trigger sources go straight to
  `dispatchSubscription` via the runtime's emit callback. The dispatch service
  is the single owner of the fan-out vs single-target decision.

cue-session-state.ts: SessionState shape changes
  - drop `timers: Timer[]`, `watchers: (() => void)[]`, and the
    `nextTriggers: Map<string, number>` projection map
  - add `triggerSources: CueTriggerSource[]`
  - `getEarliestNextTriggerIso` now iterates trigger sources and calls
    their `nextTriggerAt()` instead of reading the projection map

cue-session-runtime-service.ts: thin trigger-wiring loop
  - replaces the imperative if/else ladder with a single call to
    `createTriggerSource(sub.event, ctx)` per subscription
  - app.startup loop now uses `passesFilter` instead of inlined matchesFilter
  - drops the now-unused `executeCueRun` callback (everything goes through
    `dispatchSubscription`)
  - teardown calls `source.stop()` on each trigger source

cue-engine.ts: drops the `executeCueRun` wiring on the runtime service.

cue-subscription-setup.ts: DELETED. The 320 lines of imperative trigger setup
  + the duplicated `dispatchOrExecuteSubscription` helper are gone.

Tests
- 56 new tests across 7 files in src/__tests__/main/cue/triggers/:
  - cue-trigger-filter.test.ts          (5 tests)
  - cue-trigger-source-registry.test.ts (12 tests, every event type)
  - cue-heartbeat-trigger-source.test.ts    (9 tests)
  - cue-scheduled-trigger-source.test.ts    (8 tests)
  - cue-file-watcher-trigger-source.test.ts (7 tests)
  - cue-task-scanner-trigger-source.test.ts (7 tests)
  - cue-github-poller-trigger-source.test.ts (8 tests)
- cue-engine.test.ts: updated calculateNextScheduledTime import from the
  deleted cue-subscription-setup.ts to triggers/cue-schedule-utils.

Cue suite: 672 passing (up from 616). Full repo: 24,787 passing.
The 17 existing trigger-related integration tests (cue-startup, cue-concurrency,
cue-completion-chains, cue-multi-hop-chains, cue-engine, cue-session-lifecycle)
all pass without any assertion changes — only the architecture moved.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

📝 Walkthrough

Walkthrough

Introduces a trigger-source architecture and central session registry, moves prompt materialization into config-load (loadCueConfigDetailed), extracts recovery/sleep-reconciliation into a new service, replaces runtime prompt-file reads with materialized prompts, and updates engine startup API to start('system-boot' | ...).

Changes

Cohort / File(s) Summary
Config loading & normalization
src/main/cue/cue-yaml-loader.ts, src/main/cue/config/cue-config-normalizer.ts, src/__tests__/main/cue/cue-yaml-loader.test.ts
Added loadCueConfigDetailed discriminated result and warnings; materializeCueConfig now returns { config, warnings }; removed runtime prompt_file/output_prompt_file fields and materialized prompt/outputPrompt.
Repository & IPC storage
src/main/cue/config/cue-config-repository.ts, src/main/cue/pipeline-layout-store.ts, src/main/ipc/handlers/cue.ts, src/__tests__/main/cue/cue-config-repository.test.ts, src/__tests__/main/cue/cue-ipc-handlers.test.ts
New canonical config/prompt file APIs (writeCueConfigFile, deleteCueConfigFile, writeCuePromptFile) and pipeline-layout persistence; IPC handlers now delegate filesystem operations to repository/store; tests adapted to mock repository APIs.
Trigger-source system
src/main/cue/triggers/..., src/__tests__/main/cue/triggers/*
Introduced CueTriggerSource abstraction, registry factory, schedule utils, filter helper, and concrete trigger-source implementations (heartbeat, scheduled, file-watcher, task-scanner, GitHub poller) with corresponding tests.
Session registry & state
src/main/cue/cue-session-registry.ts, src/main/cue/cue-session-state.ts, src/__tests__/main/cue/cue-session-registry.test.ts
Added CueSessionRegistry owning session lifecycle and dedup state; SessionState now holds triggerSources[]; dedup/eviction/clear semantics centralized and tested.
Runtime & engine refactor
src/main/cue/cue-session-runtime-service.ts, src/main/cue/cue-engine.ts, src/__tests__/main/cue/*
Replaced monolithic subscription setup with trigger-sources; session runtime and engine wired to registry; loadCueConfigDetailed used for config loading; engine start API changed to start(reason: SessionInitReason) and delegates recovery to CueRecoveryService; widespread test updates.
Recovery & heartbeat
src/main/cue/cue-recovery-service.ts, src/main/cue/cue-heartbeat.ts, src/__tests__/main/cue/cue-recovery-service.test.ts
New cue-recovery-service handles DB init/prune, sleep detection, and missed-event reconciliation (exports SLEEP_THRESHOLD_MS/EVENT_PRUNE_AGE_MS); cue-heartbeat simplified to heartbeat writer only and re-exports thresholds (deprecated).
Executor & dispatch
src/main/cue/cue-executor.ts, src/main/cue/cue-dispatch-service.ts, src/__tests__/main/cue/cue-executor.test.ts
Executor no longer reads prompt files at runtime; treats promptPath as resolved inline prompt content and errors when empty. Dispatch uses materialized prompt and no longer falls back to prompt_file. Tests updated.
Removed module
src/main/cue/cue-subscription-setup.ts
Deleted monolithic subscription-setup module; functionality replaced by trigger-source modules.
Shared contracts & tests
src/shared/cue/contracts.ts, many src/__tests__/main/cue/*.test.ts
Removed prompt_file/output_prompt_file from runtime CueSubscription contract; broad test additions/adjustments reflecting new APIs and behaviors.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant Engine as CueEngine
    participant Recovery as CueRecoveryService
    participant DB as CueDB
    participant Registry as CueSessionRegistry

    App->>Engine: start('system-boot')
    Engine->>Recovery: init()
    Recovery->>DB: initCueDb()
    Recovery->>DB: pruneCueEvents()
    Recovery-->>Engine: { ok: true }
    Engine->>Registry: register(sessionId, state)
    Engine->>Engine: initSession(session, { reason: 'system-boot' })
Loading
sequenceDiagram
    participant Runtime as SessionRuntimeService
    participant Factory as TriggerSourceRegistry
    participant Source as TriggerSource
    participant Registry as CueSessionRegistry
    participant Dispatch as DispatchService

    Runtime->>Factory: createTriggerSource(eventType, ctx)
    Factory->>Source: instantiate
    Runtime->>Source: start()
    Source->>Source: poll / listen / watch
    Source->>Runtime: emit(event)
    Runtime->>Registry: markScheduledFired(...)
    Runtime->>Dispatch: dispatchSubscription(sessionId, sub, event)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

approved

Poem

🐰 I munched the YAML, chewed the prose,
Registry snug where session grows,
Triggers sprout and heartbeat hums,
Prompts are baked — no runtime runs,
Hooray! The Cue field blossoms — yum!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the three main refactoring objectives: hardening config boundary, introducing session registry, and isolating trigger sources.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cue-polish

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 9, 2026

Greptile Summary

This is a large, well-structured refactor of the Maestro Cue subsystem across three logical commits: (1) a config boundary that strips prompt_file/output_prompt_file before they reach the runtime, (2) a CueSessionRegistry that owns all dedup state, and (3) a trigger-source abstraction that replaces parallel timers[]/watchers[]/nextTriggers Map with a uniform CueTriggerSource[] interface. The refactor addresses the root causes of the recent fix cascade and the test suite grows from 24,699 to 24,787 passing. One reliability issue was found in the new pipeline-layout-store.ts.

Confidence Score: 4/5

Safe to merge after fixing the JSON.parse crash in loadPipelineLayout; all other findings are P2

One P1 defect: loadPipelineLayout throws on a corrupt layout file instead of returning null, causing every cue:loadPipelineLayout IPC call to reject persistently. The two P2 findings (double YAML parse, missing path-escape guard) are non-blocking. The structural refactor is sound — registry ownership, discriminated union config loading, and trigger source abstraction all look correct.

src/main/cue/pipeline-layout-store.ts (P1 JSON.parse); src/main/cue/config/cue-config-repository.ts (P2 path guard); src/main/cue/cue-yaml-loader.ts (P2 double parse)

Important Files Changed

Filename Overview
src/main/cue/pipeline-layout-store.ts New module owning pipeline layout persistence; missing try/catch around JSON.parse causes persistent IPC errors on corrupt layout file
src/main/cue/config/cue-config-repository.ts Single owner of all cue.yaml and prompts/ filesystem I/O; writeCuePromptFile lacks a path-escape guard on relativePath
src/main/cue/cue-yaml-loader.ts loadCueConfigDetailed returns a clean discriminated union; minor: file.raw is YAML-parsed twice (inline + inside parseCueConfigDocument)
src/main/cue/cue-session-registry.ts Well-designed single owner of session map and both dedup key sets; atomic check-and-set semantics and correct clear() semantics (preserves startup keys)
src/main/cue/cue-session-runtime-service.ts Session lifecycle correctly wires trigger sources via factory, gates app.startup on reason=system-boot, and properly separates teardown vs full removal startup-key semantics
src/main/cue/triggers/cue-trigger-source-registry.ts Clean factory switch replacing the old if/else ladder; correctly returns null for runtime-handled event types (agent.completed, app.startup)
src/main/cue/cue-recovery-service.ts Cleanly extracts DB init/prune and sleep-detection into one place; returns structured result for init failure; non-fatal shutdown
src/main/cue/cue-engine.ts Correctly renamed start(boolean) → start(SessionInitReason) with sensible default; delegates all mutable state to injected services; thin façade as intended
src/main/ipc/handlers/cue.ts Reduced to thin delegates as intended; all filesystem I/O moved to repository/store modules; withIpcErrorLogging used consistently
src/shared/cue/contracts.ts prompt_file and output_prompt_file correctly removed from CueSubscription; comment explains normalization contract

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    subgraph Config["Config Boundary (Commit 1)"]
        YAML["cue.yaml on disk"] --> Repo["cue-config-repository\nreadCueConfigFile / watchCueConfigFile"]
        Repo --> Loader["cue-yaml-loader\nloadCueConfigDetailed()"]
        Loader --> Normalizer["cue-config-normalizer\nparseCueConfigDocument()\nmaterializeCueConfig()"]
        Normalizer -->|"strips prompt_file → inline prompt\nreturns warnings"| Config_RT["CueConfig runtime model\n(no prompt_file field)"]
    end

    subgraph Registry["Session Registry (Commit 2)"]
        CueEngine["CueEngine\nstart(reason: SessionInitReason)"] --> RegObj["CueSessionRegistry\nsessions Map\nscheduledFiredKeys Set\nstartupFiredKeys Set"]
        CueEngine --> Recovery["CueRecoveryService\nDB init / prune\nsleep detection / reconcile"]
        CueEngine --> Heartbeat["CueHeartbeat\n30s writer loop"]
    end

    subgraph Triggers["Trigger Sources (Commit 3)"]
        RuntimeSvc["CueSessionRuntimeService\ninitSession / refreshSession / removeSession"] --> Factory["createTriggerSource(eventType, ctx)"]
        Factory --> HB["HeartbeatTriggerSource\nsetInterval"]
        Factory --> Sched["ScheduledTriggerSource\nsetInterval 60s + markScheduledFired"]
        Factory --> FW["FileWatcherTriggerSource\nchokidar"]
        Factory --> TS["TaskScannerTriggerSource\npoll"]
        Factory --> GH["GitHubPollerTriggerSource\npoll"]
        Factory -->|"null for runtime-handled"| Direct["app.startup / agent.completed\n(handled directly by runtime)"]
        RuntimeSvc --> TrigArr["SessionState.triggerSources[]"]
    end

    Config_RT --> RuntimeSvc
    RegObj --> RuntimeSvc
    TrigArr -->|"source.stop() loop on teardown"| Teardown["Clean teardown"]
Loading

Reviews (1): Last reviewed commit: "refactor(cue): isolate trigger sources b..." | Re-trigger Greptile

Comment on lines +29 to +35
export function loadPipelineLayout(): PipelineLayoutState | null {
const filePath = getLayoutFilePath();
if (!fs.existsSync(filePath)) {
return null;
}
const content = fs.readFileSync(filePath, 'utf-8');
return JSON.parse(content) as PipelineLayoutState;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 JSON.parse throws on a corrupt layout file

loadPipelineLayout returns null when the file is missing, but throws a SyntaxError when the file exists but is corrupt. withIpcErrorLogging re-throws the exception, so every call to cue:loadPipelineLayout will reject — the renderer gets an error instead of the graceful null path it expects — until the user manually deletes cue-pipeline-layout.json.

Suggested change
export function loadPipelineLayout(): PipelineLayoutState | null {
const filePath = getLayoutFilePath();
if (!fs.existsSync(filePath)) {
return null;
}
const content = fs.readFileSync(filePath, 'utf-8');
return JSON.parse(content) as PipelineLayoutState;
export function loadPipelineLayout(): PipelineLayoutState | null {
const filePath = getLayoutFilePath();
if (!fs.existsSync(filePath)) {
return null;
}
try {
const content = fs.readFileSync(filePath, 'utf-8');
return JSON.parse(content) as PipelineLayoutState;
} catch {
return null;
}
}

Comment on lines +43 to +68

let parsed: unknown;
try {
parsed = yaml.load(file.raw);
} catch (err) {
return {
ok: false,
reason: 'parse-error',
message: err instanceof Error ? err.message : String(err),
};
}

if (!parsed || typeof parsed !== 'object') {
return {
ok: false,
reason: 'parse-error',
message: 'Cue config root must be a YAML mapping',
};
}

const validation = validateCueConfigDocument(parsed);
if (!validation.valid) {
return { ok: false, reason: 'invalid', errors: validation.errors };
}

const document = parseCueConfigDocument(file.raw, projectRoot);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Double YAML parse in loadCueConfigDetailed

file.raw is parsed once inline via yaml.load(file.raw) (line 46) for validation, then parseCueConfigDocument(file.raw, projectRoot) (line 68) calls yaml.load a second time internally. For a config file that just passed validation this is harmless, but it's redundant work on every hot-reload. Consider passing the already-parsed object to a variant of parseCueConfigDocument that accepts pre-parsed YAML, or extract the shared parse result.

Comment on lines +86 to +101
export function writeCuePromptFile(
projectRoot: string,
relativePath: string,
content: string
): string {
const promptsDir = path.join(projectRoot, CUE_PROMPTS_DIR);
if (!fs.existsSync(promptsDir)) {
fs.mkdirSync(promptsDir, { recursive: true });
}
const absPath = path.join(projectRoot, relativePath);
const dir = path.dirname(absPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(absPath, content, 'utf-8');
return absPath;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 relativePath not validated against project root escape

path.join(projectRoot, relativePath) resolves ../ segments, so a caller that sends ../../etc/cron.d/job via the cue:writeYaml IPC message would write outside projectRoot. In practice the renderer is trusted, but a guard makes the boundary explicit:

const absPath = path.join(projectRoot, relativePath);
if (!absPath.startsWith(path.resolve(projectRoot) + path.sep)) {
    throw new Error(`writeCuePromptFile: relativePath escapes projectRoot: ${relativePath}`);
}

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (7)
src/main/cue/cue-dispatch-service.ts (1)

62-66: Prefer explicit missing-prompt guard over silent '' fallback.

Lines 65 and 80 currently mask contract drift by dispatching empty prompts. Consider logging and skipping when prompt is unexpectedly missing.

Suggested refactor
 					const perTargetPrompt = sub.fan_out_prompts?.[i];
-					const prompt = perTargetPrompt ?? sub.prompt ?? '';
+					const prompt = perTargetPrompt ?? sub.prompt;
+					if (prompt === undefined) {
+						deps.onLog('error', `[CUE] Missing prompt for subscription "${sub.name}"`);
+						continue;
+					}
@@
 			deps.executeRun(
 				ownerSessionId,
-				sub.prompt ?? '',
+				sub.prompt,
 				event,
 				sub.name,
 				sub.output_prompt,
 				chainDepth
 			);

Also applies to: 78-81

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/cue/cue-dispatch-service.ts` around lines 62 - 66, The code
currently falls back to an empty string for a missing prompt when building
`prompt` from `perTargetPrompt ?? sub.prompt ?? ''`; change this to explicitly
detect a missing prompt (both `perTargetPrompt` and `sub.prompt`
undefined/null/empty) and skip the `deps.executeRun` call, logging a warning
that includes identifying info (e.g., `sub` id and target index `i`) so contract
drift is visible; apply the same guard where `prompt` is used later (lines
around use of `perTargetPrompt`/`sub.prompt` and the other occurrence at 78–81)
to avoid dispatching empty prompts.
src/__tests__/main/cue/cue-session-lifecycle.test.ts (1)

583-601: Last startup-key test does not verify the “can re-fire” claim yet.

The test currently proves “no re-fire on refresh/user-toggle,” but not re-fire on a later system-boot after re-adding the session.

💡 Suggested assertion extension
 		engine.refreshSession('session-1', '/projects/test');
 		expect(deps.onCueRun).toHaveBeenCalledTimes(1);
 
+		// After re-add, a real system boot should be eligible to fire startup again.
+		engine.stop();
+		engine.start('system-boot');
+		expect(deps.onCueRun).toHaveBeenCalledTimes(2);
+
 		engine.stop();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/cue/cue-session-lifecycle.test.ts` around lines 583 - 601,
Extend the test so it also verifies that after removing the session and
refreshing (which shouldn't re-fire), a subsequent real boot does re-trigger
startup: after the existing refreshSession('session-1', '/projects/test')
assertion, call engine.start('system-boot') again and assert deps.onCueRun has
been called one more time (e.g., toHaveBeenCalledTimes(2)); use the same
CueEngine instance and existing helpers (makeStartupConfig, createMockDeps,
mockLoadCueConfig) and keep the final engine.stop().
src/main/cue/triggers/cue-trigger-source-registry.ts (1)

32-50: Add an explicit fallback return path for unsupported event values.

Without a fallback, unsupported runtime values can implicitly return undefined instead of null and skip observability.

💡 Proposed hardening
 	switch (eventType) {
 		case 'time.heartbeat':
 			return createCueHeartbeatTriggerSource(ctx);
@@
 		case 'app.startup':
 			// These are not timer/watcher-driven — the runtime handles them
 			// directly via the completion service / startup loop.
 			return null;
+		default:
+			ctx.onLog('cue', `[CUE] Unsupported trigger event type: "${String(eventType)}"`);
+			return null;
 	}
 }
As per coding guidelines: "Do not silently swallow errors. Let unhandled exceptions bubble up to Sentry for error tracking in production. Handle expected/recoverable errors explicitly."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/cue/triggers/cue-trigger-source-registry.ts` around lines 32 - 50,
The switch over eventType (which returns various createCue*TriggerSource
functions like createCueHeartbeatTriggerSource, createCueScheduledTriggerSource,
createCueFileWatcherTriggerSource, createCueTaskScannerTriggerSource and
createCueGitHubPollerTriggerSource) currently has no default branch and can
implicitly return undefined for unsupported event values; add an explicit
default case that returns null and emits an observability signal (e.g.
ctx.logger.warn or console.warn) referencing the unknown eventType so
unsupported events are handled explicitly rather than silently producing
undefined.
src/__tests__/main/cue/triggers/cue-trigger-source-registry.test.ts (1)

111-125: Add a negative case for GitHub subscriptions missing repo.

The suite currently validates positive GitHub paths but doesn’t protect against misconfigured GitHub subscriptions. Adding explicit repo-missing assertions would prevent regressions in factory guard behavior.

Proposed test additions
+	it('returns null for github.pull_request missing repo', () => {
+		const source = createTriggerSource(
+			'github.pull_request',
+			makeCtx(baseSub('github.pull_request'))
+		);
+		expect(source).toBeNull();
+	});
+
+	it('returns null for github.issue missing repo', () => {
+		const source = createTriggerSource('github.issue', makeCtx(baseSub('github.issue')));
+		expect(source).toBeNull();
+	});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/cue/triggers/cue-trigger-source-registry.test.ts` around
lines 111 - 125, Add negative tests that assert createTriggerSource returns null
when GitHub subscriptions lack the required repo field: add two new cases
mirroring the existing positives but use makeCtx(baseSub('github.pull_request',
{})) and makeCtx(baseSub('github.issue', {})) (or equivalent missing-repo
payloads) and expect the resulting source toBeNull(); place these next to the
existing tests that use createTriggerSource, makeCtx, and baseSub so the factory
guard behavior for github.pull_request and github.issue is covered for missing
repo.
src/__tests__/main/cue/cue-engine.test.ts (1)

1768-1777: Tighten the invalid-time test expectation.

The current assertion accepts both outcomes, so it won’t catch accidental rollover scheduling behavior. Prefer asserting null for invalid time inputs to make this test protective.

Proposed test update
 		it('returns null for invalid time strings', () => {
 			vi.setSystemTime(new Date('2026-03-09T08:00:00'));
 			const result = calculateNextScheduledTime(['25:99']);
-			// Invalid hours/minutes — parseInt yields 25 and 99, but the resulting
-			// Date will roll over. The function still produces a candidate because
-			// Date constructor handles overflow. Check it doesn't crash.
-			// With hour=25, the date rolls to next day 01:XX — still a valid timestamp.
-			// This is acceptable behavior (no crash), but let's verify it returns something.
-			expect(typeof result === 'number' || result === null).toBe(true);
+			expect(result).toBeNull();
 		});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/cue/cue-engine.test.ts` around lines 1768 - 1777, The test
for invalid time strings should assert that
calculateNextScheduledTime(['25:99']) returns null rather than allowing any
number; update the assertion in the test case (the one that calls
calculateNextScheduledTime with ['25:99'] and sets system time via
vi.setSystemTime) to expect strict null (e.g., expect(result).toBeNull()) so the
test fails if the function accidentally accepts rollover times instead of
treating them as invalid.
src/main/cue/cue-session-registry.ts (1)

22-30: The registry still leaks mutable session state.

get() and snapshot() hand out live SessionState objects, so callers can still mutate lastTriggered, triggerSources, and similar fields outside the registry. That weakens the “sole owner” boundary this refactor is trying to create. Consider returning readonly views here and adding narrow mutation APIs for the fields that legitimately need to change.

Also applies to: 89-99

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/cue/cue-session-registry.ts` around lines 22 - 30, get() and
snapshot() currently leak live mutable SessionState objects; change
CueSessionRegistry so get(sessionId) returns a readonly/defensive copy (or an
immutable ReadonlySessionState view) and snapshot() returns a Map of
readonly/session-state copies to prevent external mutation, and add narrow
mutation APIs (e.g., updateLastTriggered(sessionId, newTimestamp),
addTriggerSource(sessionId, source), removeTriggerSource(sessionId, source)) to
allow controlled updates; update register/unregister implementations to store
the canonical mutable state internally while exposing only readonly views via
get/snapshot and adjust any callers to use the new mutation methods.
src/__tests__/main/cue/cue-yaml-loader.test.ts (1)

349-353: Avoid depending on validator error ordering in this test.

validateCueConfig() returns a list, so checking result.errors[0] makes this fail if another validation starts reporting first. Matching the collection keeps the test focused on behavior instead of ordering.

Suggested fix
 			expect(result.ok).toBe(false);
 			if (!result.ok && result.reason === 'invalid') {
 				expect(result.errors.length).toBeGreaterThan(0);
-				expect(result.errors[0]).toMatch(/interval_minutes/);
+				expect(result.errors).toEqual(
+					expect.arrayContaining([expect.stringMatching(/interval_minutes/)])
+				);
 			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/cue/cue-yaml-loader.test.ts` around lines 349 - 353, The
test relies on error ordering by asserting result.errors[0]; change the
assertion to check the collection contains an error matching /interval_minutes/
instead of indexing into result.errors so it passes regardless of ordering —
update the assertion that references result.errors (in the cue-yaml-loader.test
around the validateCueConfig call) to use a collection-containing matcher that
looks for a string matching /interval_minutes/ (e.g., expect.arrayContaining /
expect.stringMatching style) while keeping the existing checks for result.ok and
result.reason.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/cue/config/cue-config-normalizer.ts`:
- Around line 193-201: The warning messages for prompt file resolution
(conditions using sub.promptSpec.file && !sub.promptSpec.inline && !sub.prompt
and sub.outputPromptSpec?.file && !sub.outputPromptSpec.inline &&
sub.output_prompt == null) overstate the failure as "file was not found"; update
the warnings.push text for both instances to reflect that the file either was
not found or resolved to empty/unreadable content (e.g., say "file was not found
or resolved to empty/unreadable content — subscription will fail on trigger" for
the prompt case and a similar wording for the output_prompt case) so the message
matches the actual failure modes detected by the existing condition.

In `@src/main/cue/config/cue-config-repository.ts`:
- Around line 86-101: The writeCuePromptFile function currently trusts
relativePath and can write outside CUE_PROMPTS_DIR; to fix, validate and
constrain the target to the prompts directory: reject absolute paths and
normalize the combined path using path.resolve(promptsDir, relativePath) (or
similar) then verify the resolved path starts with the resolved promptsDir
prefix; if the check fails, throw an error and do not write. After validation,
create parent dirs and write to the resolved path (use the resolved variable
instead of joining projectRoot + relativePath) so all filesystem writes are
confined to the .maestro/prompts boundary.

In `@src/main/cue/cue-executor.ts`:
- Around line 142-143: The current check in cue-executor.ts only tests falsy
promptContent, allowing whitespace-only prompts like '  ' to pass; change the
validation to treat whitespace-only as missing by trimming before checking:
e.g., compute a trimmedPrompt = promptContent?.trim() and then guard with if
(!trimmedPrompt) (or if (!promptContent || promptContent.trim().length === 0)),
and use trimmedPrompt (or keep using promptContent after validating) where you
need non-empty prompt content; reference the existing promptContent and
subscription.name symbols when updating the error message and subsequent logic.

In `@src/main/cue/cue-recovery-service.ts`:
- Around line 103-113: The catch blocks in the sleep-detection recovery code and
in shutdown() currently swallow errors (the catch in the try around sleep
detection and the try/catch inside shutdown() around closeCueDb()), so update
them to report errors to Sentry and/or the logging pipeline before re-throwing
or propagating as appropriate: call Sentry.captureException(error) (or
captureMessage with normalized error text) and deps.onLog('error', ...) with a
normalized message including the function context (e.g., "CUE sleep detection
failed" and "CUE shutdown/closeCueDb failed"), then re-throw or remove the
silent swallow so Sentry can record the failure; ensure you reference the
try/catch that wraps sleep detection and the shutdown() implementation that
calls closeCueDb().

In `@src/main/cue/cue-session-runtime-service.ts`:
- Around line 119-179: Register the session state with
registry.register(session.id, state) before starting any trigger sources or
firing app.startup so other components (e.g., CueRunManager via
registry.get(sessionId)) see the session settings; move the call to
registry.register up to just after state.yamlWatcher is assigned and before the
for-loop that creates/starts sources and before the app.startup loop
(references: SessionState state, registry.register, createTriggerSource /
source.start, registry.markStartupFired).

In `@src/main/cue/pipeline-layout-store.ts`:
- Around line 29-36: In loadPipelineLayout(), guard the JSON.parse call so
malformed/partial JSON becomes a recoverable load failure: wrap the
fs.readFileSync + JSON.parse logic in a try/catch, return null on parse errors,
and call the Sentry helper(s) (captureException and/or captureMessage from
src/utils/sentry.ts) with context including the filePath and a short message
that the layout JSON is corrupted; keep the existing behavior of returning the
parsed PipelineLayoutState when successful and only treat other unexpected
errors similarly if they indicate corruption.

In `@src/main/cue/triggers/cue-heartbeat-trigger-source.ts`:
- Around line 26-35: The heartbeat handler updates nextFireMs only when an event
is emitted, so when passesFilter(ctx.subscription, event, ctx.onLog) returns
false nextTriggerAt() remains stuck; change fire(...) to advance nextFireMs =
Date.now() + intervalMs before the early return (i.e., set nextFireMs
immediately after logging/creating the event and before the passesFilter return
path), and apply the same fix to the other similar filtered-path (the block
around the second emits at the end of the file) so nextFireMs is always moved
forward even when ctx.emit is skipped.

In `@src/main/cue/triggers/cue-schedule-utils.ts`:
- Around line 41-50: The code parses hour/min from time but doesn't validate
bounds, allowing values outside 0–23 and 0–59 which Date.setHours will normalize
and produce rollover times; in the logic around time.split(':') / parseInt (the
hour, min variables) add explicit checks that hour is an integer between 0 and
23 and min is an integer between 0 and 59 (skip/continue on invalid), and only
then call target.setHours(hour, min, 0, 0) before comparing target.getTime()
with now and pushing to candidates.

In `@src/main/cue/triggers/cue-scheduled-trigger-source.ts`:
- Around line 76-79: The start() function currently waits for the first interval
tick and can miss a scheduled occurrence if created mid-minute; update start()
to perform an immediate check of the current minute before arming the timer: if
timer is falsy, run the same logic that checkAndFire() would use for the current
moment (or call checkAndFire() once) to detect and call markScheduledFired() for
a slot in the same minute, then set timer = setInterval(checkAndFire,
POLL_INTERVAL_MS) and call recomputeNextFire(); preserve the idempotent guard
(if (timer) return) and ensure this immediate check uses the same date/slot
computation as checkAndFire() to avoid duplicate firing.

In `@src/main/ipc/handlers/cue.ts`:
- Around line 221-223: Validate and sanitize options.promptFiles before calling
writeCuePromptFile: for each relativePath from options.promptFiles, reject or
normalize any absolute paths or paths containing ".." and compute const target =
path.resolve(options.projectRoot, ".maestro/prompts", relativePath); then verify
target.startsWith(path.resolve(options.projectRoot,
".maestro/prompts")+path.sep) (or equals the base) and only call
writeCuePromptFile with the original relativePath (or the safe relative from the
base) if the check passes; otherwise log/throw and skip writing. Use functions
path.resolve, path.normalize, and path.sep and reference options.promptFiles,
options.projectRoot and writeCuePromptFile to locate where to apply this check.
- Around line 219-224: The current flow calls writeCueConfigFile(...) before
writing prompt files, which can leave cue.yaml referencing missing prompt files
if a subsequent writeCuePromptFile(...) fails; change the order so you first
iterate over options.promptFiles and call
writeCuePromptFile(options.projectRoot, relativePath, content) for each
(propagating any errors immediately), and only after all prompt files succeed
call writeCueConfigFile(options.projectRoot, options.content) to persist
cue.yaml. Ensure you preserve existing error propagation behavior so failures
abort before writeCueConfigFile is invoked.

---

Nitpick comments:
In `@src/__tests__/main/cue/cue-engine.test.ts`:
- Around line 1768-1777: The test for invalid time strings should assert that
calculateNextScheduledTime(['25:99']) returns null rather than allowing any
number; update the assertion in the test case (the one that calls
calculateNextScheduledTime with ['25:99'] and sets system time via
vi.setSystemTime) to expect strict null (e.g., expect(result).toBeNull()) so the
test fails if the function accidentally accepts rollover times instead of
treating them as invalid.

In `@src/__tests__/main/cue/cue-session-lifecycle.test.ts`:
- Around line 583-601: Extend the test so it also verifies that after removing
the session and refreshing (which shouldn't re-fire), a subsequent real boot
does re-trigger startup: after the existing refreshSession('session-1',
'/projects/test') assertion, call engine.start('system-boot') again and assert
deps.onCueRun has been called one more time (e.g., toHaveBeenCalledTimes(2));
use the same CueEngine instance and existing helpers (makeStartupConfig,
createMockDeps, mockLoadCueConfig) and keep the final engine.stop().

In `@src/__tests__/main/cue/cue-yaml-loader.test.ts`:
- Around line 349-353: The test relies on error ordering by asserting
result.errors[0]; change the assertion to check the collection contains an error
matching /interval_minutes/ instead of indexing into result.errors so it passes
regardless of ordering — update the assertion that references result.errors (in
the cue-yaml-loader.test around the validateCueConfig call) to use a
collection-containing matcher that looks for a string matching
/interval_minutes/ (e.g., expect.arrayContaining / expect.stringMatching style)
while keeping the existing checks for result.ok and result.reason.

In `@src/__tests__/main/cue/triggers/cue-trigger-source-registry.test.ts`:
- Around line 111-125: Add negative tests that assert createTriggerSource
returns null when GitHub subscriptions lack the required repo field: add two new
cases mirroring the existing positives but use
makeCtx(baseSub('github.pull_request', {})) and makeCtx(baseSub('github.issue',
{})) (or equivalent missing-repo payloads) and expect the resulting source
toBeNull(); place these next to the existing tests that use createTriggerSource,
makeCtx, and baseSub so the factory guard behavior for github.pull_request and
github.issue is covered for missing repo.

In `@src/main/cue/cue-dispatch-service.ts`:
- Around line 62-66: The code currently falls back to an empty string for a
missing prompt when building `prompt` from `perTargetPrompt ?? sub.prompt ??
''`; change this to explicitly detect a missing prompt (both `perTargetPrompt`
and `sub.prompt` undefined/null/empty) and skip the `deps.executeRun` call,
logging a warning that includes identifying info (e.g., `sub` id and target
index `i`) so contract drift is visible; apply the same guard where `prompt` is
used later (lines around use of `perTargetPrompt`/`sub.prompt` and the other
occurrence at 78–81) to avoid dispatching empty prompts.

In `@src/main/cue/cue-session-registry.ts`:
- Around line 22-30: get() and snapshot() currently leak live mutable
SessionState objects; change CueSessionRegistry so get(sessionId) returns a
readonly/defensive copy (or an immutable ReadonlySessionState view) and
snapshot() returns a Map of readonly/session-state copies to prevent external
mutation, and add narrow mutation APIs (e.g., updateLastTriggered(sessionId,
newTimestamp), addTriggerSource(sessionId, source),
removeTriggerSource(sessionId, source)) to allow controlled updates; update
register/unregister implementations to store the canonical mutable state
internally while exposing only readonly views via get/snapshot and adjust any
callers to use the new mutation methods.

In `@src/main/cue/triggers/cue-trigger-source-registry.ts`:
- Around line 32-50: The switch over eventType (which returns various
createCue*TriggerSource functions like createCueHeartbeatTriggerSource,
createCueScheduledTriggerSource, createCueFileWatcherTriggerSource,
createCueTaskScannerTriggerSource and createCueGitHubPollerTriggerSource)
currently has no default branch and can implicitly return undefined for
unsupported event values; add an explicit default case that returns null and
emits an observability signal (e.g. ctx.logger.warn or console.warn) referencing
the unknown eventType so unsupported events are handled explicitly rather than
silently producing undefined.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d66124b6-8546-4bab-9435-617fd36ad44b

📥 Commits

Reviewing files that changed from the base of the PR and between b97da37 and 9eda3b6.

📒 Files selected for processing (46)
  • src/__tests__/main/cue/cue-completion-chains.test.ts
  • src/__tests__/main/cue/cue-concurrency.test.ts
  • src/__tests__/main/cue/cue-config-repository.test.ts
  • src/__tests__/main/cue/cue-engine.test.ts
  • src/__tests__/main/cue/cue-executor.test.ts
  • src/__tests__/main/cue/cue-ipc-handlers.test.ts
  • src/__tests__/main/cue/cue-multi-hop-chains.test.ts
  • src/__tests__/main/cue/cue-recovery-service.test.ts
  • src/__tests__/main/cue/cue-session-lifecycle.test.ts
  • src/__tests__/main/cue/cue-session-registry.test.ts
  • src/__tests__/main/cue/cue-sleep-prevention.test.ts
  • src/__tests__/main/cue/cue-sleep-wake.test.ts
  • src/__tests__/main/cue/cue-startup.test.ts
  • src/__tests__/main/cue/cue-yaml-loader.test.ts
  • src/__tests__/main/cue/triggers/cue-file-watcher-trigger-source.test.ts
  • src/__tests__/main/cue/triggers/cue-github-poller-trigger-source.test.ts
  • src/__tests__/main/cue/triggers/cue-heartbeat-trigger-source.test.ts
  • src/__tests__/main/cue/triggers/cue-scheduled-trigger-source.test.ts
  • src/__tests__/main/cue/triggers/cue-task-scanner-trigger-source.test.ts
  • src/__tests__/main/cue/triggers/cue-trigger-filter.test.ts
  • src/__tests__/main/cue/triggers/cue-trigger-source-registry.test.ts
  • src/main/cue/config/cue-config-normalizer.ts
  • src/main/cue/config/cue-config-repository.ts
  • src/main/cue/cue-dispatch-service.ts
  • src/main/cue/cue-engine.ts
  • src/main/cue/cue-executor.ts
  • src/main/cue/cue-heartbeat.ts
  • src/main/cue/cue-recovery-service.ts
  • src/main/cue/cue-session-registry.ts
  • src/main/cue/cue-session-runtime-service.ts
  • src/main/cue/cue-session-state.ts
  • src/main/cue/cue-subscription-setup.ts
  • src/main/cue/cue-yaml-loader.ts
  • src/main/cue/pipeline-layout-store.ts
  • src/main/cue/triggers/cue-file-watcher-trigger-source.ts
  • src/main/cue/triggers/cue-github-poller-trigger-source.ts
  • src/main/cue/triggers/cue-heartbeat-trigger-source.ts
  • src/main/cue/triggers/cue-schedule-utils.ts
  • src/main/cue/triggers/cue-scheduled-trigger-source.ts
  • src/main/cue/triggers/cue-task-scanner-trigger-source.ts
  • src/main/cue/triggers/cue-trigger-filter.ts
  • src/main/cue/triggers/cue-trigger-source-registry.ts
  • src/main/cue/triggers/cue-trigger-source.ts
  • src/main/index.ts
  • src/main/ipc/handlers/cue.ts
  • src/shared/cue/contracts.ts
💤 Files with no reviewable changes (1)
  • src/main/cue/cue-subscription-setup.ts

Inline fixes:
- cue-config-normalizer: warning text now says "not found or resolved to
  empty/unreadable content" instead of just "not found" for both prompt
  and output_prompt file cases
- cue-config-repository: writeCuePromptFile rejects absolute paths and
  validates the resolved path stays within .maestro/prompts/
- cue-executor: whitespace-only prompt strings treated as missing via trim()
- cue-recovery-service: both silent catch blocks now call captureException
  with context before continuing
- cue-session-runtime-service: registry.register() moved before trigger
  source creation so other components see the session immediately
- pipeline-layout-store: JSON.parse wrapped in try/catch; parse errors
  return null and report to Sentry
- cue-heartbeat-trigger-source: nextFireMs advanced before passesFilter
  so nextTriggerAt() stays current even when the filter rejects
- cue-schedule-utils: added hour 0-23 and minute 0-59 bounds validation
- cue-scheduled-trigger-source: start() calls checkAndFire() immediately
  before arming the interval to avoid missing a mid-minute start
- cue.ts IPC handler: prompt files validated and written before config YAML;
  absolute/traversal paths are rejected

Nitpick fixes:
- cue-engine.test: invalid time test asserts toBeNull(); three GitHub tests
  get missing repo fields; scheduled no-match test uses a non-matching minute
- cue-session-lifecycle.test: startup re-fire test includes stop()/start()
  cycle; double-count test updated for registry-before-sources behavior
- cue-yaml-loader.test: error assertion uses arrayContaining instead of [0]
- cue-trigger-source-registry.test: two negative tests for GitHub subs
  missing repo field
- cue-dispatch-service: missing/empty prompts skip executeRun with a warning
- cue-trigger-source-registry: explicit default branch with never assertion
- cue-github-poller-trigger-source: returns null when repo is missing
- cue-config-repository.test: nested-path test expects single mkdirSync call
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
src/__tests__/main/cue/triggers/cue-trigger-source-registry.test.ts (2)

56-148: Consider parameterizing repetitive event matrix cases.

This suite is very repetitive; a small table-driven structure (it.each) would reduce maintenance overhead as new event types are added.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/cue/triggers/cue-trigger-source-registry.test.ts` around
lines 56 - 148, The test file repeats many similar assertions for
createTriggerSource using makeCtx and baseSub; convert these into table-driven
tests using Jest's it.each to reduce duplication. Create an array of cases
describing eventName, subscriptionOverrides (e.g., { interval_minutes: 5 } or {
watch: '**/*.ts' } or { repo: 'foo/bar' }), and expectedNull boolean, then
replace the repeated it blocks with two parameterized suites (one for positive
cases where expectedNull=false asserting source not null and function types for
start/stop/nextTriggerAt when applicable, and one for negative cases where
expectedNull=true asserting source is null) that call
createTriggerSource(eventName, makeCtx(baseSub(eventName,
subscriptionOverrides))). Ensure references to createTriggerSource, makeCtx, and
baseSub are used so test behavior remains identical.

55-138: Assert provider selection explicitly, not only nullability.

These tests can still pass if two event mappings are accidentally swapped but both return non-null sources with similar prerequisites. Add assertions on the mocked provider factory calls so each event validates the concrete branch taken.

✅ Example strengthening patch
-import { describe, it, expect, vi } from 'vitest';
+import { beforeEach, describe, it, expect, vi } from 'vitest';
 import { createTriggerSource } from '../../../../main/cue/triggers/cue-trigger-source-registry';
 import { createCueSessionRegistry } from '../../../../main/cue/cue-session-registry';
 import type { CueEventType, CueSubscription } from '../../../../main/cue/cue-types';
 import type { CueTriggerSourceContext } from '../../../../main/cue/triggers/cue-trigger-source';
+import { createCueFileWatcher } from '../../../../main/cue/cue-file-watcher';
+import { createCueTaskScanner } from '../../../../main/cue/cue-task-scanner';
+import { createCueGitHubPoller } from '../../../../main/cue/cue-github-poller';
@@
 vi.mock('../../../../main/cue/cue-github-poller', () => ({
 	createCueGitHubPoller: vi.fn(() => vi.fn()),
 }));
+
+beforeEach(() => {
+	vi.clearAllMocks();
+});
@@
 	it('returns a source for file.changed with watch glob', () => {
 		const source = createTriggerSource(
 			'file.changed',
 			makeCtx(baseSub('file.changed', { watch: '**/*.ts' }))
 		);
 		expect(source).not.toBeNull();
+		expect(createCueFileWatcher).toHaveBeenCalledTimes(1);
+		expect(createCueTaskScanner).not.toHaveBeenCalled();
+		expect(createCueGitHubPoller).not.toHaveBeenCalled();
 	});
@@
 	it('returns a source for task.pending with watch glob', () => {
 		const source = createTriggerSource(
 			'task.pending',
 			makeCtx(baseSub('task.pending', { watch: '**/*.md' }))
 		);
 		expect(source).not.toBeNull();
+		expect(createCueTaskScanner).toHaveBeenCalledTimes(1);
+		expect(createCueFileWatcher).not.toHaveBeenCalled();
+		expect(createCueGitHubPoller).not.toHaveBeenCalled();
 	});
@@
 	it('returns a source for github.pull_request', () => {
 		const source = createTriggerSource(
 			'github.pull_request',
 			makeCtx(baseSub('github.pull_request', { repo: 'foo/bar' }))
 		);
 		expect(source).not.toBeNull();
+		expect(createCueGitHubPoller).toHaveBeenCalledTimes(1);
 	});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/cue/triggers/cue-trigger-source-registry.test.ts` around
lines 55 - 138, The tests currently only assert nullability of
createTriggerSource results, which won’t catch swapped provider branches; after
each createTriggerSource(...) call (using makeCtx and baseSub) add explicit
assertions that the mocked provider factory was invoked for the expected
provider key/branch so the concrete provider branch is validated (e.g., assert
provider factory mock was called with the provider identifier for
time.heartbeat, time.scheduled, file.changed, task.pending, github.pull_request,
github.issue respectively); reference the existing symbols createTriggerSource,
makeCtx, and baseSub when locating where to add the
expect(...providerFactoryMock...).toHaveBeenCalledWith(...) assertions and use
expect.anything() for irrelevant args if needed.
src/__tests__/main/cue/cue-config-repository.test.ts (1)

173-216: Consider adding tests for path validation edge cases.

The writeCuePromptFile implementation includes important security checks (rejecting absolute paths and paths that escape .maestro/prompts/), but this test suite doesn't cover those branches.

💡 Suggested additional test cases
it('rejects absolute paths', () => {
	expect(() =>
		writeCuePromptFile(PROJECT_ROOT, '/etc/passwd', 'content')
	).toThrow('relativePath must be relative');
});

it('rejects paths that escape the prompts directory', () => {
	expect(() =>
		writeCuePromptFile(PROJECT_ROOT, '.maestro/other/file.md', 'content')
	).toThrow('resolves outside the prompts directory');
});

it('rejects traversal attempts', () => {
	expect(() =>
		writeCuePromptFile(PROJECT_ROOT, '.maestro/prompts/../../etc/passwd', 'content')
	).toThrow('resolves outside the prompts directory');
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/cue/cue-config-repository.test.ts` around lines 173 - 216,
Tests are missing coverage for path validation branches in writeCuePromptFile;
add unit tests that assert it throws for absolute paths, for paths that resolve
outside the prompts directory (e.g., sibling .maestro/other/file.md), and for
traversal attempts (e.g., .maestro/prompts/../../etc/passwd). Locate
writeCuePromptFile in the cue-config-repository tests and add three specs that
call writeCuePromptFile(PROJECT_ROOT, <badPath>, 'content') and expect a thrown
error with messages indicating "relativePath must be relative" or "resolves
outside the prompts directory" to exercise those security checks (use the same
error text your implementation throws). Ensure mocks for fs functions remain as
in other tests so only validation triggers the exceptions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/__tests__/main/cue/cue-config-repository.test.ts`:
- Around line 173-216: Tests are missing coverage for path validation branches
in writeCuePromptFile; add unit tests that assert it throws for absolute paths,
for paths that resolve outside the prompts directory (e.g., sibling
.maestro/other/file.md), and for traversal attempts (e.g.,
.maestro/prompts/../../etc/passwd). Locate writeCuePromptFile in the
cue-config-repository tests and add three specs that call
writeCuePromptFile(PROJECT_ROOT, <badPath>, 'content') and expect a thrown error
with messages indicating "relativePath must be relative" or "resolves outside
the prompts directory" to exercise those security checks (use the same error
text your implementation throws). Ensure mocks for fs functions remain as in
other tests so only validation triggers the exceptions.

In `@src/__tests__/main/cue/triggers/cue-trigger-source-registry.test.ts`:
- Around line 56-148: The test file repeats many similar assertions for
createTriggerSource using makeCtx and baseSub; convert these into table-driven
tests using Jest's it.each to reduce duplication. Create an array of cases
describing eventName, subscriptionOverrides (e.g., { interval_minutes: 5 } or {
watch: '**/*.ts' } or { repo: 'foo/bar' }), and expectedNull boolean, then
replace the repeated it blocks with two parameterized suites (one for positive
cases where expectedNull=false asserting source not null and function types for
start/stop/nextTriggerAt when applicable, and one for negative cases where
expectedNull=true asserting source is null) that call
createTriggerSource(eventName, makeCtx(baseSub(eventName,
subscriptionOverrides))). Ensure references to createTriggerSource, makeCtx, and
baseSub are used so test behavior remains identical.
- Around line 55-138: The tests currently only assert nullability of
createTriggerSource results, which won’t catch swapped provider branches; after
each createTriggerSource(...) call (using makeCtx and baseSub) add explicit
assertions that the mocked provider factory was invoked for the expected
provider key/branch so the concrete provider branch is validated (e.g., assert
provider factory mock was called with the provider identifier for
time.heartbeat, time.scheduled, file.changed, task.pending, github.pull_request,
github.issue respectively); reference the existing symbols createTriggerSource,
makeCtx, and baseSub when locating where to add the
expect(...providerFactoryMock...).toHaveBeenCalledWith(...) assertions and use
expect.anything() for irrelevant args if needed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5e22dc37-254b-41e7-ba03-46687c7674e6

📥 Commits

Reviewing files that changed from the base of the PR and between 9eda3b6 and ecd72cb.

📒 Files selected for processing (18)
  • src/__tests__/main/cue/cue-config-repository.test.ts
  • src/__tests__/main/cue/cue-engine.test.ts
  • src/__tests__/main/cue/cue-session-lifecycle.test.ts
  • src/__tests__/main/cue/cue-yaml-loader.test.ts
  • src/__tests__/main/cue/triggers/cue-trigger-source-registry.test.ts
  • src/main/cue/config/cue-config-normalizer.ts
  • src/main/cue/config/cue-config-repository.ts
  • src/main/cue/cue-dispatch-service.ts
  • src/main/cue/cue-executor.ts
  • src/main/cue/cue-recovery-service.ts
  • src/main/cue/cue-session-runtime-service.ts
  • src/main/cue/pipeline-layout-store.ts
  • src/main/cue/triggers/cue-github-poller-trigger-source.ts
  • src/main/cue/triggers/cue-heartbeat-trigger-source.ts
  • src/main/cue/triggers/cue-schedule-utils.ts
  • src/main/cue/triggers/cue-scheduled-trigger-source.ts
  • src/main/cue/triggers/cue-trigger-source-registry.ts
  • src/main/ipc/handlers/cue.ts
🚧 Files skipped from review as they are similar to previous changes (10)
  • src/main/cue/cue-dispatch-service.ts
  • src/main/cue/pipeline-layout-store.ts
  • src/main/cue/triggers/cue-trigger-source-registry.ts
  • src/main/cue/cue-executor.ts
  • src/main/cue/triggers/cue-heartbeat-trigger-source.ts
  • src/main/cue/triggers/cue-github-poller-trigger-source.ts
  • src/main/cue/triggers/cue-schedule-utils.ts
  • src/main/cue/triggers/cue-scheduled-trigger-source.ts
  • src/tests/main/cue/cue-engine.test.ts
  • src/main/cue/cue-session-runtime-service.ts

…ce tests

- cue-config-repository.test: add three security specs for writeCuePromptFile
  covering absolute paths, sibling-directory escapes, and traversal attempts;
  each asserts the expected error message and that writeFileSync is never called

- cue-trigger-source-registry.test: convert repeated it blocks into two
  parameterized suites using it.each — POSITIVE_CASES (non-null source, correct
  shape) and NULL_CASES (missing required fields or runtime-handled types);
  import the three mocked provider factories (createCueFileWatcher,
  createCueTaskScanner, createCueGitHubPoller) and add factory-invocation
  assertions inside the positive suite by calling source.start() and checking
  the correct provider branch was taken; add beforeEach(vi.clearAllMocks) so
  call counts are isolated between parameterized runs
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/__tests__/main/cue/triggers/cue-trigger-source-registry.test.ts (2)

111-118: Also assert non-selected factories are untouched.

Current assertions confirm the expected branch was called, but not that only that branch was called. Adding negative assertions will tighten regression detection for routing bugs.

Proposed assertion hardening
 			const factoryMocks: Record<NonNullable<FactoryKey>, ReturnType<typeof vi.fn>> = {
 				file: vi.mocked(createCueFileWatcher),
 				task: vi.mocked(createCueTaskScanner),
 				github: vi.mocked(createCueGitHubPoller),
 			};
-			if (factoryKey !== null) {
-				expect(factoryMocks[factoryKey]).toHaveBeenCalledOnce();
-			}
+			for (const [key, mock] of Object.entries(factoryMocks) as Array<
+				[NonNullable<FactoryKey>, ReturnType<typeof vi.fn>]
+			>) {
+				if (factoryKey === key) {
+					expect(mock).toHaveBeenCalledOnce();
+				} else {
+					expect(mock).not.toHaveBeenCalled();
+				}
+			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/cue/triggers/cue-trigger-source-registry.test.ts` around
lines 111 - 118, The test currently asserts the selected factory was called but
not that the others were untouched; modify the assertion block around
factoryMocks/FactoryKey to also assert non-selected factories were not called:
when factoryKey !== null, assert factoryMocks[factoryKey] was calledOnce and for
each other key in {file, task, github}
expect(factoryMocks[other]).not.toHaveBeenCalled(); when factoryKey === null
assert all factoryMocks (createCueFileWatcher, createCueTaskScanner,
createCueGitHubPoller) were not called. Use the existing factoryMocks and
factoryKey identifiers to implement these negative assertions.

99-121: Guarantee stop() runs even when assertions fail.

start() can register timers/watchers; if an assertion fails before cleanup, teardown may be skipped and create cross-test flakiness. Wrap the running/assertion block in try/finally.

Proposed fix
 		it.each(POSITIVE_CASES)('$event', ({ event, overrides, factoryKey }) => {
 			const source = createTriggerSource(event, makeCtx(baseSub(event, overrides)));

 			expect(source).not.toBeNull();
 			expect(typeof source!.start).toBe('function');
 			expect(typeof source!.stop).toBe('function');
 			expect(typeof source!.nextTriggerAt).toBe('function');

-			// Start the source so provider factory calls are triggered, then verify
-			// the correct branch was taken for event types backed by a mocked provider.
-			source!.start();
-
-			const factoryMocks: Record<NonNullable<FactoryKey>, ReturnType<typeof vi.fn>> = {
-				file: vi.mocked(createCueFileWatcher),
-				task: vi.mocked(createCueTaskScanner),
-				github: vi.mocked(createCueGitHubPoller),
-			};
-			if (factoryKey !== null) {
-				expect(factoryMocks[factoryKey]).toHaveBeenCalledOnce();
-			}
-
-			source!.stop();
+			try {
+				// Start the source so provider factory calls are triggered, then verify
+				// the correct branch was taken for event types backed by a mocked provider.
+				source!.start();
+
+				const factoryMocks: Record<NonNullable<FactoryKey>, ReturnType<typeof vi.fn>> = {
+					file: vi.mocked(createCueFileWatcher),
+					task: vi.mocked(createCueTaskScanner),
+					github: vi.mocked(createCueGitHubPoller),
+				};
+				if (factoryKey !== null) {
+					expect(factoryMocks[factoryKey]).toHaveBeenCalledOnce();
+				}
+			} finally {
+				source!.stop();
+			}
 		});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/cue/triggers/cue-trigger-source-registry.test.ts` around
lines 99 - 121, The test currently calls source!.start(), runs assertions and
then source!.stop(), but if any assertion fails stop() may never run; modify the
it.each(POSITIVE_CASES) test body (the block that creates source via
createTriggerSource and calls source!.start()) to wrap the execution and
assertions in a try/finally so that source!.stop() is always invoked in the
finally block; ensure the factoryMocks checks
(expect(factoryMocks[factoryKey]).toHaveBeenCalledOnce()) remain inside the try
so failures still trigger cleanup and keep references to createTriggerSource,
source.start, and source.stop unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/__tests__/main/cue/triggers/cue-trigger-source-registry.test.ts`:
- Around line 111-118: The test currently asserts the selected factory was
called but not that the others were untouched; modify the assertion block around
factoryMocks/FactoryKey to also assert non-selected factories were not called:
when factoryKey !== null, assert factoryMocks[factoryKey] was calledOnce and for
each other key in {file, task, github}
expect(factoryMocks[other]).not.toHaveBeenCalled(); when factoryKey === null
assert all factoryMocks (createCueFileWatcher, createCueTaskScanner,
createCueGitHubPoller) were not called. Use the existing factoryMocks and
factoryKey identifiers to implement these negative assertions.
- Around line 99-121: The test currently calls source!.start(), runs assertions
and then source!.stop(), but if any assertion fails stop() may never run; modify
the it.each(POSITIVE_CASES) test body (the block that creates source via
createTriggerSource and calls source!.start()) to wrap the execution and
assertions in a try/finally so that source!.stop() is always invoked in the
finally block; ensure the factoryMocks checks
(expect(factoryMocks[factoryKey]).toHaveBeenCalledOnce()) remain inside the try
so failures still trigger cleanup and keep references to createTriggerSource,
source.start, and source.stop unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fdcf9ff1-7a86-4202-bf99-99c135e67d8e

📥 Commits

Reviewing files that changed from the base of the PR and between ecd72cb and ebe83e9.

📒 Files selected for processing (2)
  • src/__tests__/main/cue/cue-config-repository.test.ts
  • src/__tests__/main/cue/triggers/cue-trigger-source-registry.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/tests/main/cue/cue-config-repository.test.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant