refactor(launcher): consolidate mpv socket readiness primitive

This commit is contained in:
2026-02-21 13:35:55 -08:00
parent a693cc1866
commit 2b77ab2406
7 changed files with 123 additions and 45 deletions

View File

@@ -1,10 +1,10 @@
--- ---
id: TASK-73 id: TASK-73
title: Consolidate launcher mpv socket readiness primitives title: Consolidate launcher mpv socket readiness primitives
status: To Do status: Done
assignee: [] assignee: []
created_date: '2026-02-18 11:35' created_date: '2026-02-18 11:35'
updated_date: '2026-02-18 11:35' updated_date: '2026-02-21 20:19'
labels: labels:
- launcher - launcher
- mpv - mpv
@@ -31,15 +31,14 @@ Launcher contains multiple overlapping MPV readiness/polling helpers (`waitForSo
## Acceptance Criteria ## Acceptance Criteria
<!-- AC:BEGIN --> <!-- AC:BEGIN -->
- [ ] #1 Single canonical MPV socket readiness helper remains - [x] #1 Single canonical MPV socket readiness helper remains
- [ ] #2 All launcher callsites use unified helper - [x] #2 All launcher callsites use unified helper
- [ ] #3 Behavior/exit code compatibility maintained for CLI flows - [x] #3 Behavior/exit code compatibility maintained for CLI flows
- [ ] #4 Test coverage added for readiness timeout behavior - [x] #4 Test coverage added for readiness timeout behavior
<!-- AC:END --> <!-- AC:END -->
## Definition of Done ## Definition of Done
<!-- DOD:BEGIN --> <!-- DOD:BEGIN -->
- [ ] #1 Launcher tests pass - [x] #1 Launcher tests pass
- [ ] #2 No dead helper functions remain - [x] #2 No dead helper functions remain
<!-- DOD:END --> <!-- DOD:END -->

View File

@@ -38,3 +38,8 @@ Read first. Keep concise.
| `opencode-task97-runtime-composer-20260221T094150Z-r8k3` | `opencode-task97-runtime-composer` | `Execute TASK-97 normalize runtime composer contracts end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task97-runtime-composer-20260221T094150Z-r8k3.md` | `2026-02-21T10:06:59Z` | | `opencode-task97-runtime-composer-20260221T094150Z-r8k3` | `opencode-task97-runtime-composer` | `Execute TASK-97 normalize runtime composer contracts end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task97-runtime-composer-20260221T094150Z-r8k3.md` | `2026-02-21T10:06:59Z` |
| `opencode-task96-config-resolve-20260221T094119Z-mbfo` | `opencode-task96-config-resolve` | `Execute TASK-96 split config resolve into domain modules with plan-first workflow` | `planning` | `docs/subagents/agents/opencode-task96-config-resolve-20260221T094119Z-mbfo.md` | `2026-02-21T09:41:19Z` | | `opencode-task96-config-resolve-20260221T094119Z-mbfo` | `opencode-task96-config-resolve` | `Execute TASK-96 split config resolve into domain modules with plan-first workflow` | `planning` | `docs/subagents/agents/opencode-task96-config-resolve-20260221T094119Z-mbfo.md` | `2026-02-21T09:41:19Z` |
| `opencode-task98-source-tests-20260221T094524Z-kzvd` | `opencode-task98-source-tests` | `Execute TASK-98 shift core tests to source level and trim dist coupling without commit` | `blocked` | `docs/subagents/agents/opencode-task98-source-tests-20260221T094524Z-kzvd.md` | `2026-02-21T09:56:47Z` | | `opencode-task98-source-tests-20260221T094524Z-kzvd` | `opencode-task98-source-tests` | `Execute TASK-98 shift core tests to source level and trim dist coupling without commit` | `blocked` | `docs/subagents/agents/opencode-task98-source-tests-20260221T094524Z-kzvd.md` | `2026-02-21T09:56:47Z` |
| `codex-task96-config-resolve-20260221T110058Z-k7m2` | `codex-task96-config-resolve` | `Execute TASK-96 split config resolve into domain modules end-to-end without commit` | `done` | `docs/subagents/agents/codex-task96-config-resolve-20260221T110058Z-k7m2.md` | `2026-02-21T20:10:43Z` |
| `codex-task73-mpv-socket-20260221T201605Z-zjhs` | `codex-task73-mpv-socket` | `Execute TASK-73 consolidate launcher mpv socket readiness primitives end-to-end` | `done` | `docs/subagents/agents/codex-task73-mpv-socket-20260221T201605Z-zjhs.md` | `2026-02-21T20:20:18Z` |
| `codex-task74-launcher-tests-20260221T201635Z-10i6` | `codex-task74-launcher-tests` | `Implement TASK-74 launcher regression tests for config discovery + command branching end-to-end` | `done` | `docs/subagents/agents/codex-task74-launcher-tests-20260221T201635Z-10i6.md` | `2026-02-21T20:20:52Z` |
| `opencode-task76-anki-workflows-20260221T201659Z-r4p1` | `opencode-task76-anki-workflows` | `Execute TASK-76 decompose anki-integration orchestrator into workflow services via plan-first workflow` | `done` | `docs/subagents/agents/opencode-task76-anki-workflows-20260221T201659Z-r4p1.md` | `2026-02-21T21:17:28Z` |
| `opencode-task76-doc-boundaries-20260221T203558Z-h7q4` | `opencode-task76-doc-boundaries` | `Update Anki integration docs with post-decomposition ownership boundaries for TASK-76` | `done` | `docs/subagents/agents/opencode-task76-doc-boundaries-20260221T203558Z-h7q4.md` | `2026-02-21T20:36:55Z` |

View File

@@ -0,0 +1,33 @@
# Agent: `codex-task73-mpv-socket-20260221T201605Z-zjhs`
- alias: `codex-task73-mpv-socket`
- mission: `Execute TASK-73 consolidate launcher mpv socket readiness primitives end-to-end`
- status: `done`
- branch: `main`
- started_at: `2026-02-21T20:16:05Z`
- heartbeat_minutes: `5`
## Current Work (newest first)
- [2026-02-21T20:20:18Z] test: `bun run build` fails on unrelated pre-existing missing modules in `src/anki-integration/field-grouping-workflow.test.ts` and `src/anki-integration/note-update-workflow.test.ts`; not caused by TASK-73 scope.
- [2026-02-21T20:19:24Z] handoff: Completed TASK-73; unified launcher socket readiness on `waitForUnixSocketReady`, removed duplicate helper(s), updated launcher callsites, added `launcher/mpv.test.ts`, and marked backlog task Done.
- [2026-02-21T20:19:24Z] test: `bun test launcher/mpv.test.ts launcher/config.test.ts launcher/parse-args.test.ts` pass; `make build-launcher` pass.
- [2026-02-21T20:19:24Z] progress: Removed `waitForSocket` and inlined path-existence polling into canonical `waitForUnixSocketReady` loop; switched overlay startup gate in `launcher/main.ts` to unified helper.
- [2026-02-21T20:18:42Z] progress: Mapped duplicate readiness primitives in `launcher/mpv.ts` (`waitForSocket`, `waitForPathExists`, `waitForUnixSocketReady`) and all callsites in `launcher/main.ts` + `launcher/jellyfin.ts`.
- [2026-02-21T20:16:05Z] intent: Load task acceptance criteria and map current launcher socket-readiness call graph before edits.
## Files Touched
- `docs/subagents/INDEX.md`
- `docs/subagents/agents/codex-task73-mpv-socket-20260221T201605Z-zjhs.md`
- `launcher/mpv.ts`
- `launcher/main.ts`
- `launcher/mpv.test.ts`
- `backlog/tasks/task-73 - Consolidate-launcher-mpv-socket-readiness-primitives.md`
## Assumptions
- Backlog MCP unavailable in this repo state; local `backlog/tasks/task-73 - Consolidate-launcher-mpv-socket-readiness-primitives.md` is source of truth.
## Open Questions / Blockers
- None.
## Next Step
- Await user review or follow-up tasks.

View File

@@ -40,3 +40,11 @@ Shared notes. Append-only.
- [2026-02-21T09:41:19Z] [opencode-task96-config-resolve-20260221T094119Z-mbfo|opencode-task96-config-resolve] starting TASK-96 via Backlog MCP + writing-plans/executing-plans workflow; scope expected around `src/config/resolve.ts`, new config-resolve domain modules, seam tests, and budget checks. - [2026-02-21T09:41:19Z] [opencode-task96-config-resolve-20260221T094119Z-mbfo|opencode-task96-config-resolve] starting TASK-96 via Backlog MCP + writing-plans/executing-plans workflow; scope expected around `src/config/resolve.ts`, new config-resolve domain modules, seam tests, and budget checks.
- [2026-02-21T09:45:24Z] [opencode-task98-source-tests-20260221T094524Z-kzvd|opencode-task98-source-tests] starting TASK-98 via Backlog MCP + writing-plans/executing-plans workflow; targeting source-level test entrypoints and dist-coupling cleanup, no commit. - [2026-02-21T09:45:24Z] [opencode-task98-source-tests-20260221T094524Z-kzvd|opencode-task98-source-tests] starting TASK-98 via Backlog MCP + writing-plans/executing-plans workflow; targeting source-level test entrypoints and dist-coupling cleanup, no commit.
- [2026-02-21T09:56:47Z] [opencode-task98-source-tests-20260221T094524Z-kzvd|opencode-task98-source-tests] TASK-98 implementation pass complete: source test lane (`test:fast`) moved to `bun test` source entrypoints, explicit `test:smoke:dist` added, CI/release updated, docs+timing evidence recorded; blocked final DoD on unrelated pre-existing `bun run build` TS errors from in-flight TASK-96/97 files. - [2026-02-21T09:56:47Z] [opencode-task98-source-tests-20260221T094524Z-kzvd|opencode-task98-source-tests] TASK-98 implementation pass complete: source test lane (`test:fast`) moved to `bun test` source entrypoints, explicit `test:smoke:dist` added, CI/release updated, docs+timing evidence recorded; blocked final DoD on unrelated pre-existing `bun run build` TS errors from in-flight TASK-96/97 files.
- [2026-02-21T11:00:58Z] [codex-task96-config-resolve-20260221T110058Z-k7m2|codex-task96-config-resolve] taking over TASK-96 execution: load backlog task, write plan via writing-plans, execute via executing-plans, and finalize AC/DoD evidence without commit.
- [2026-02-21T20:10:43Z] [codex-task96-config-resolve-20260221T110058Z-k7m2|codex-task96-config-resolve] completed TASK-96: `src/config/resolve.ts` reduced to thin orchestrator (33 LOC), config resolve seam tests wired into src+dist config lanes via `package.json`, required gates green (`build`, `test:config:dist`, `check:file-budgets`), and backlog task marked Done with metrics evidence.
- [2026-02-21T20:20:52Z] [codex-task74-launcher-tests-20260221T201635Z-10i6|codex-task74-launcher-tests] completed TASK-74: added `launcher/main.test.ts` regression harness for config discovery + command branching (`doctor`, `config`, `mpv`, `jellyfin`), wired launcher tests into `test:launcher` + `test:core:src`, updated launcher docs test command, and marked backlog task done.
- [2026-02-21T20:19:24Z] [codex-task73-mpv-socket-20260221T201605Z-zjhs|codex-task73-mpv-socket] completed TASK-73: consolidated launcher MPV socket readiness on `waitForUnixSocketReady`, removed `waitForSocket`/duplicate path polling, rewired launcher overlay callsite, added readiness regression tests (`launcher/mpv.test.ts`), launcher tests + `make build-launcher` green.
- [2026-02-21T20:20:18Z] [codex-task73-mpv-socket-20260221T201605Z-zjhs|codex-task73-mpv-socket] full `bun run build` currently blocked by unrelated missing modules in `src/anki-integration/field-grouping-workflow.test.ts` and `src/anki-integration/note-update-workflow.test.ts` (outside TASK-73 scope).
- [2026-02-21T20:16:59Z] [opencode-task76-anki-workflows-20260221T201659Z-r4p1|opencode-task76-anki-workflows] starting TASK-76 via Backlog MCP + writing-plans/executing-plans; likely scope `src/anki-integration.ts` + new `src/anki-integration/*` workflow services, with overlap checks before edits.
- [2026-02-21T20:35:58Z] [opencode-task76-doc-boundaries-20260221T203558Z-h7q4|opencode-task76-doc-boundaries] overlap note: TASK-76 already has an active planning agent; this pass is docs-only (`docs/anki-integration.md`) to capture ownership boundaries after workflow decomposition.
- [2026-02-21T21:16:18Z] [opencode-task76-anki-workflows-20260221T201659Z-r4p1|opencode-task76-anki-workflows] completed TASK-76: extracted `note-update-workflow` + `field-grouping-workflow` services, delegated facade hotpaths in `src/anki-integration.ts`, added focused workflow seam tests, docs ownership boundaries updated, `bun run build && bun run test:core:dist` green, and backlog TASK-76 marked Done.

View File

@@ -19,7 +19,6 @@ import {
stopOverlay, stopOverlay,
launchTexthookerOnly, launchTexthookerOnly,
findAppBinary, findAppBinary,
waitForSocket,
loadSubtitleIntoMpv, loadSubtitleIntoMpv,
runAppCommandWithInherit, runAppCommandWithInherit,
launchMpvIdleDetached, launchMpvIdleDetached,
@@ -361,7 +360,7 @@ async function main(): Promise<void> {
}); });
} }
const ready = await waitForSocket(mpvSocketPath); const ready = await waitForUnixSocketReady(mpvSocketPath, 10000);
const shouldStartOverlay = args.startOverlay || args.autoStartOverlay; const shouldStartOverlay = args.startOverlay || args.autoStartOverlay;
if (shouldStartOverlay) { if (shouldStartOverlay) {
if (ready) { if (ready) {

61
launcher/mpv.test.ts Normal file
View File

@@ -0,0 +1,61 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import net from 'node:net';
import { EventEmitter } from 'node:events';
import { waitForUnixSocketReady } from './mpv';
import * as mpvModule from './mpv';
function createTempSocketPath(): { dir: string; socketPath: string } {
const baseDir = path.join(process.cwd(), '.tmp', 'launcher-mpv-tests');
fs.mkdirSync(baseDir, { recursive: true });
const dir = fs.mkdtempSync(path.join(baseDir, 'case-'));
return { dir, socketPath: path.join(dir, 'mpv.sock') };
}
test('mpv module exposes only canonical socket readiness helper', () => {
assert.equal('waitForSocket' in mpvModule, false);
});
test('waitForUnixSocketReady returns false when socket never appears', async () => {
const { dir, socketPath } = createTempSocketPath();
try {
const ready = await waitForUnixSocketReady(socketPath, 120);
assert.equal(ready, false);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
test('waitForUnixSocketReady returns false when path exists but is not socket', async () => {
const { dir, socketPath } = createTempSocketPath();
try {
fs.writeFileSync(socketPath, 'not-a-socket');
const ready = await waitForUnixSocketReady(socketPath, 200);
assert.equal(ready, false);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
test('waitForUnixSocketReady returns true when socket becomes connectable before timeout', async () => {
const { dir, socketPath } = createTempSocketPath();
fs.writeFileSync(socketPath, '');
const originalCreateConnection = net.createConnection;
try {
net.createConnection = (() => {
const socket = new EventEmitter() as net.Socket;
socket.destroy = (() => socket) as net.Socket['destroy'];
socket.setTimeout = (() => socket) as net.Socket['setTimeout'];
setTimeout(() => socket.emit('connect'), 25);
return socket;
}) as typeof net.createConnection;
const ready = await waitForUnixSocketReady(socketPath, 400);
assert.equal(ready, true);
} finally {
net.createConnection = originalCreateConnection;
fs.rmSync(dir, { recursive: true, force: true });
}
});

View File

@@ -416,23 +416,6 @@ export async function loadSubtitleIntoMpv(
} }
} }
export function waitForSocket(socketPath: string, timeoutMs = 10000): Promise<boolean> {
const start = Date.now();
return new Promise((resolve) => {
const timer = setInterval(() => {
if (fs.existsSync(socketPath)) {
clearInterval(timer);
resolve(true);
return;
}
if (Date.now() - start >= timeoutMs) {
clearInterval(timer);
resolve(false);
}
}, 100);
});
}
export function startMpv( export function startMpv(
target: string, target: string,
targetKind: 'file' | 'url', targetKind: 'file' | 'url',
@@ -672,19 +655,6 @@ async function sleepMs(ms: number): Promise<void> {
await new Promise<void>((resolve) => setTimeout(resolve, ms)); await new Promise<void>((resolve) => setTimeout(resolve, ms));
} }
async function waitForPathExists(filePath: string, timeoutMs: number): Promise<boolean> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try {
if (fs.existsSync(filePath)) return true;
} catch {
// ignore transient fs errors
}
await sleepMs(150);
}
return false;
}
async function canConnectUnixSocket(socketPath: string): Promise<boolean> { async function canConnectUnixSocket(socketPath: string): Promise<boolean> {
return await new Promise<boolean>((resolve) => { return await new Promise<boolean>((resolve) => {
const socket = net.createConnection(socketPath); const socket = net.createConnection(socketPath);
@@ -713,10 +683,13 @@ export async function waitForUnixSocketReady(
): Promise<boolean> { ): Promise<boolean> {
const deadline = Date.now() + timeoutMs; const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) { while (Date.now() < deadline) {
const exists = await waitForPathExists(socketPath, 300); try {
if (exists) { if (fs.existsSync(socketPath)) {
const ready = await canConnectUnixSocket(socketPath); const ready = await canConnectUnixSocket(socketPath);
if (ready) return true; if (ready) return true;
}
} catch {
// ignore transient fs errors
} }
await sleepMs(150); await sleepMs(150);
} }