mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-10-21 05:01:26 -07:00
refactor(tests): centralize lane definitions and add per-file isolation (#143)
This commit is contained in:
1 parent
8b0ef662bc
commit
ad1d240f20
16 files changed
+369
-160
No files matched your search
@@ -55,9 +55,6 @@ jobs:
|
|||||||
- name: Verify generated config examples
|
- name: Verify generated config examples
|
||||||
run: bun run verify:config-example
|
run: bun run verify:config-example
|
||||||
|
|
||||||
- name: Internal docs knowledge-base checks
|
|
||||||
run: bun run test:docs:kb
|
|
||||||
|
|
||||||
- name: Test suite (source)
|
- name: Test suite (source)
|
||||||
run: bun run test:fast
|
run: bun run test:fast
|
||||||
|
|
||||||
@@ -71,6 +68,9 @@ jobs:
|
|||||||
path: coverage/test-src/lcov.info
|
path: coverage/test-src/lcov.info
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Stats UI tests
|
||||||
|
run: bun run test:stats
|
||||||
|
|
||||||
- name: Launcher smoke suite (source)
|
- name: Launcher smoke suite (source)
|
||||||
run: bun run test:launcher:smoke:src
|
run: bun run test:launcher:smoke:src
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ jobs:
|
|||||||
path: coverage/test-src/lcov.info
|
path: coverage/test-src/lcov.info
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Stats UI tests
|
||||||
|
run: bun run test:stats
|
||||||
|
|
||||||
- name: Launcher smoke suite (source)
|
- name: Launcher smoke suite (source)
|
||||||
run: bun run test:launcher:smoke:src
|
run: bun run test:launcher:smoke:src
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ jobs:
|
|||||||
path: coverage/test-src/lcov.info
|
path: coverage/test-src/lcov.info
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Stats UI tests
|
||||||
|
run: bun run test:stats
|
||||||
|
|
||||||
- name: Launcher smoke suite (source)
|
- name: Launcher smoke suite (source)
|
||||||
run: bun run test:launcher:smoke:src
|
run: bun run test:launcher:smoke:src
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,10 @@ Start here, then leave this file.
|
|||||||
- Config/schema/defaults: `bun run test:config`; if template/defaults changed, `bun run generate:config-example`
|
- Config/schema/defaults: `bun run test:config`; if template/defaults changed, `bun run generate:config-example`
|
||||||
- Launcher/plugin: `bun run test:launcher` or `bun run test:env`
|
- Launcher/plugin: `bun run test:launcher` or `bun run test:env`
|
||||||
- Runtime-compat / dist-sensitive: `bun run test:runtime:compat`
|
- Runtime-compat / dist-sensitive: `bun run test:runtime:compat`
|
||||||
|
- Stats dashboard UI (`stats/`): `bun run test:stats`
|
||||||
|
- Build/release scripts (`scripts/**`): `bun run test:scripts`
|
||||||
- Docs-only: `bun run docs:test`, then `bun run docs:build`
|
- Docs-only: `bun run docs:test`, then `bun run docs:build`
|
||||||
|
- Test lanes are directory-discovered via `scripts/test-lanes.ts`; never hand-list test files in `package.json`
|
||||||
|
|
||||||
## Docs Upkeep
|
## Docs Upkeep
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
type: internal
|
||||||
|
area: testing
|
||||||
|
|
||||||
|
- Test lanes are now defined once in `scripts/test-lanes.ts` and discovered by directory instead of hand-maintained file lists in `package.json`; the unused `test:core:*`/`test:config:dist`/`test:full` scripts were removed.
|
||||||
|
- `scripts/run-test-lane.mjs` runs each test file in an isolated `bun test` process with a wall timeout, so a hanging test or leaked global can no longer cascade failures across the lane.
|
||||||
|
- Previously orphaned suites now run in CI: the stats dashboard tests (`bun run test:stats`), the `scripts/**` tests (`bun run test:scripts`, including the change-verification skill tests), the `test-plugin-process-start-retries.lua` plugin test, and the runtime-compat dist slice (now part of `bun run test:fast`).
|
||||||
|
- The change-verification skill gained a `stats` lane for `stats/` edits.
|
||||||
@@ -80,18 +80,24 @@ Default lanes:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run test # alias for test:fast
|
bun run test # alias for test:fast
|
||||||
bun run test:fast # default fast lane
|
bun run test:fast # full source lanes: src + launcher-unit + scripts + runtime compat
|
||||||
bun run test:full # maintained source + launcher-unit + runtime compat surface
|
|
||||||
bun run test:runtime:compat # compiled/runtime compatibility slice only
|
bun run test:runtime:compat # compiled/runtime compatibility slice only
|
||||||
bun run test:env # launcher/plugin + env-sensitive verification
|
bun run test:env # launcher/plugin + env-sensitive verification
|
||||||
|
bun run test:stats # stats dashboard UI suite
|
||||||
bun run test:immersion:sqlite # SQLite persistence lane
|
bun run test:immersion:sqlite # SQLite persistence lane
|
||||||
bun run test:subtitle # maintained alass/ffsubsync subtitle surface
|
bun run test:subtitle # maintained alass/ffsubsync subtitle surface
|
||||||
```
|
```
|
||||||
|
|
||||||
- `bun run test` and `bun run test:fast` cover config/core suites plus representative entry/runtime, Anki integration, release-workflow coverage, typecheck, and runtime-registry checks.
|
Test lane membership is defined once in `scripts/test-lanes.ts` and discovered by
|
||||||
- `bun run test:full` is the maintained full surface: Bun-compatible `src/**` discovery, Bun-compatible launcher unit discovery, and the compiled/runtime compatibility lane for suites routed through `dist/**`.
|
directory, so new test files join their lane automatically. `scripts/run-test-lane.mjs`
|
||||||
|
runs each test file in its own `bun test` process (per-file isolation) so a hanging
|
||||||
|
test or leaked global in one file cannot cascade into the rest of the lane; pass
|
||||||
|
`--jobs N` to parallelize or `--single-process` for one shared process.
|
||||||
|
|
||||||
|
- `bun run test` and `bun run test:fast` cover the full discovered `src/**` suite, launcher unit tests, `scripts/**` tests, and the compiled/runtime compatibility lane.
|
||||||
- `bun run test:runtime:compat` covers the compiled/runtime slice directly: `ipc`, `anki-jimaku-ipc`, `overlay-manager`, `config-validation`, `startup-config`, and `registry`.
|
- `bun run test:runtime:compat` covers the compiled/runtime slice directly: `ipc`, `anki-jimaku-ipc`, `overlay-manager`, `config-validation`, `startup-config`, and `registry`.
|
||||||
- `bun run test:env` covers environment-sensitive checks: launcher smoke/plugin verification plus the Bun source SQLite lane.
|
- `bun run test:env` covers environment-sensitive checks: launcher smoke/plugin verification plus the Bun source SQLite lane.
|
||||||
|
- `bun run test:stats` runs the stats dashboard suite under `stats/src/**`.
|
||||||
- `bun run test:immersion:sqlite` is the reproducible persistence lane when you need real DB-backed SQLite coverage under Bun.
|
- `bun run test:immersion:sqlite` is the reproducible persistence lane when you need real DB-backed SQLite coverage under Bun.
|
||||||
|
|
||||||
The Bun-managed discovery lanes intentionally exclude a small compiled/runtime-focused set: `src/core/services/ipc.test.ts`, `src/core/services/anki-jimaku-ipc.test.ts`, `src/core/services/overlay-manager.test.ts`, `src/main/config-validation.test.ts`, `src/main/runtime/startup-config.test.ts`, and `src/main/runtime/registry.test.ts`. `bun run test:runtime:compat` keeps them in the standard workflow via `dist/**`.
|
The Bun-managed discovery lanes intentionally exclude a small compiled/runtime-focused set: `src/core/services/ipc.test.ts`, `src/core/services/anki-jimaku-ipc.test.ts`, `src/core/services/overlay-manager.test.ts`, `src/main/config-validation.test.ts`, `src/main/runtime/startup-config.test.ts`, and `src/main/runtime/registry.test.ts`. `bun run test:runtime:compat` keeps them in the standard workflow via `dist/**`.
|
||||||
@@ -126,11 +132,11 @@ Focused commands:
|
|||||||
```bash
|
```bash
|
||||||
bun run test:config # Source-level config schema/validation tests
|
bun run test:config # Source-level config schema/validation tests
|
||||||
bun run test:launcher # Launcher regression tests (config discovery + command routing)
|
bun run test:launcher # Launcher regression tests (config discovery + command routing)
|
||||||
bun run test:core # Source-level core regression tests (default lane)
|
|
||||||
bun run test:launcher:smoke:src # Launcher e2e smoke: launcher -> mpv IPC -> overlay start/stop wiring
|
bun run test:launcher:smoke:src # Launcher e2e smoke: launcher -> mpv IPC -> overlay start/stop wiring
|
||||||
bun run test:launcher:env:src # Launcher smoke + Lua plugin gate
|
bun run test:launcher:env:src # Launcher smoke + Lua plugin gate
|
||||||
bun run test:src # Bun-managed maintained src/** discovery lane
|
bun run test:src # Bun-managed maintained src/** discovery lane
|
||||||
bun run test:launcher:unit:src # Bun-managed maintained launcher unit lane
|
bun run test:launcher:unit:src # Bun-managed maintained launcher unit lane
|
||||||
|
bun run test:scripts # Bun-managed scripts/** test lane
|
||||||
bun run test:immersion:sqlite:src # Bun source lane
|
bun run test:immersion:sqlite:src # Bun source lane
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -144,8 +150,6 @@ Smoke and optional deep dist commands:
|
|||||||
bun run build # compile dist artifacts
|
bun run build # compile dist artifacts
|
||||||
bun run test:immersion:sqlite # compile + run SQLite-backed immersion tests under Bun
|
bun run test:immersion:sqlite # compile + run SQLite-backed immersion tests under Bun
|
||||||
bun run test:smoke:dist # explicit smoke scope for compiled runtime
|
bun run test:smoke:dist # explicit smoke scope for compiled runtime
|
||||||
bun run test:config:dist # optional full dist config suite
|
|
||||||
bun run test:core:dist # optional full dist core suite
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `bun run test:immersion:sqlite` when you need real DB-backed coverage for the immersion tracker.
|
Use `bun run test:immersion:sqlite` when you need real DB-backed coverage for the immersion tracker.
|
||||||
|
|||||||
@@ -3,10 +3,22 @@
|
|||||||
# Verification
|
# Verification
|
||||||
|
|
||||||
Status: active
|
Status: active
|
||||||
Last verified: 2026-05-23
|
Last verified: 2026-07-06
|
||||||
Owner: Kyle Yasuda
|
Owner: Kyle Yasuda
|
||||||
Read when: selecting the right verification lane for a change
|
Read when: selecting the right verification lane for a change
|
||||||
|
|
||||||
|
## Lane Infrastructure
|
||||||
|
|
||||||
|
- Lane membership is defined once in `scripts/test-lanes.ts` and discovered by
|
||||||
|
directory — new test files join their lane automatically; never hand-list test
|
||||||
|
files in `package.json`.
|
||||||
|
- `scripts/run-test-lane.mjs` runs each test file in its own `bun test` process
|
||||||
|
(per-file isolation with a wall timeout) so a hanging test or leaked global in
|
||||||
|
one file cannot cascade into the rest of the lane. `--jobs N` parallelizes;
|
||||||
|
`--single-process` restores the shared-process mode for debugging.
|
||||||
|
- `bun run test:fast` is the full source gate: discovered `src/**`, launcher
|
||||||
|
unit, `scripts/**`, and the compiled runtime-compat slice.
|
||||||
|
|
||||||
## Default Handoff Gate
|
## Default Handoff Gate
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -31,6 +43,8 @@ bun run docs:build
|
|||||||
- Config/schema/defaults: `bun run test:config`, then `bun run generate:config-example` if template/defaults changed
|
- Config/schema/defaults: `bun run test:config`, then `bun run generate:config-example` if template/defaults changed
|
||||||
- Launcher/plugin: `bun run test:launcher` or `bun run test:env`
|
- Launcher/plugin: `bun run test:launcher` or `bun run test:env`
|
||||||
- Runtime-compat / compiled behavior: `bun run test:runtime:compat`
|
- Runtime-compat / compiled behavior: `bun run test:runtime:compat`
|
||||||
|
- Stats dashboard UI: `bun run test:stats`
|
||||||
|
- Build/release scripts (`scripts/**`): `bun run test:scripts`
|
||||||
- Coverage for the maintained source lane: `bun run test:coverage:src`
|
- Coverage for the maintained source lane: `bun run test:coverage:src`
|
||||||
- Deep/local full gate: default handoff gate above
|
- Deep/local full gate: default handoff gate above
|
||||||
|
|
||||||
|
|||||||
+49
-16
@@ -65,11 +65,36 @@ function makeTestEnv(homeDir: string, xdgConfigHome: string): NodeJS.ProcessEnv
|
|||||||
APPDATA: xdgConfigHome,
|
APPDATA: xdgConfigHome,
|
||||||
LOCALAPPDATA: path.join(homeDir, 'AppData', 'Local'),
|
LOCALAPPDATA: path.join(homeDir, 'AppData', 'Local'),
|
||||||
XDG_CONFIG_HOME: xdgConfigHome,
|
XDG_CONFIG_HOME: xdgConfigHome,
|
||||||
|
// Pin the data dir under the temp home so the Linux runtime-plugin preflight
|
||||||
|
// resolves managed asset paths deterministically (not the CI runner's).
|
||||||
|
XDG_DATA_HOME: path.join(homeDir, '.local', 'share'),
|
||||||
PATH: pathValue,
|
PATH: pathValue,
|
||||||
Path: pathValue,
|
Path: pathValue,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On Linux the playback path runs `ensureLinuxRuntimePluginAvailable`, which —
|
||||||
|
// when the runtime plugin/theme are missing — spawns the app with
|
||||||
|
// `--ensure-linux-runtime-plugin-assets` and polls up to 30s
|
||||||
|
// (RESPONSE_TIMEOUT_MS) for an install response. A fake app that just exits
|
||||||
|
// never writes that response, so the launcher hangs and the test times out on
|
||||||
|
// Linux CI (the preflight is a no-op on macOS/Windows). This shell prelude makes
|
||||||
|
// the fake app install the managed plugin/theme and write the response, matching
|
||||||
|
// launcher/smoke.e2e.test.ts. Prepend it to each fake app that reaches playback.
|
||||||
|
const RUNTIME_PLUGIN_PREFLIGHT_SH = `if [ "$1" = "--ensure-linux-runtime-plugin-assets" ]; then
|
||||||
|
data="\${XDG_DATA_HOME:-$HOME/.local/share}/SubMiner"
|
||||||
|
mkdir -p "$data/plugin/subminer" "$data/themes"
|
||||||
|
printf -- '-- test plugin\\n' > "$data/plugin/subminer/main.lua"
|
||||||
|
printf 'test=true\\n' > "$data/plugin/subminer.conf"
|
||||||
|
printf '/* test theme */\\n' > "$data/themes/subminer.rasi"
|
||||||
|
if [ "$2" = "--ensure-linux-runtime-plugin-assets-response-path" ] && [ -n "$3" ]; then
|
||||||
|
mkdir -p "$(dirname "$3")"
|
||||||
|
printf '{"ok":true,"status":"installed","path":"%s"}' "$data/plugin/subminer/main.lua" > "$3"
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
`;
|
||||||
|
|
||||||
test('config path uses XDG_CONFIG_HOME override', () => {
|
test('config path uses XDG_CONFIG_HOME override', () => {
|
||||||
withTempDir((root) => {
|
withTempDir((root) => {
|
||||||
const xdgConfigHome = path.join(root, 'xdg');
|
const xdgConfigHome = path.join(root, 'xdg');
|
||||||
@@ -237,7 +262,7 @@ test('doctor refresh-known-words forwards app refresh command without requiring
|
|||||||
const capturePath = path.join(root, 'captured-args.txt');
|
const capturePath = path.join(root, 'captured-args.txt');
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
appPath,
|
appPath,
|
||||||
'#!/bin/sh\nif [ -n "$SUBMINER_TEST_CAPTURE" ]; then printf "%s\\n" "$@" > "$SUBMINER_TEST_CAPTURE"; fi\nexit 0\n',
|
`#!/bin/sh\n${RUNTIME_PLUGIN_PREFLIGHT_SH}if [ -n "$SUBMINER_TEST_CAPTURE" ]; then printf "%s\\n" "$@" > "$SUBMINER_TEST_CAPTURE"; fi\nexit 0\n`,
|
||||||
);
|
);
|
||||||
fs.chmodSync(appPath, 0o755);
|
fs.chmodSync(appPath, 0o755);
|
||||||
|
|
||||||
@@ -264,7 +289,7 @@ test('launcher settings option forwards app settings window command', () => {
|
|||||||
const capturePath = path.join(root, 'captured-args.txt');
|
const capturePath = path.join(root, 'captured-args.txt');
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
appPath,
|
appPath,
|
||||||
'#!/bin/sh\nif [ -n "$SUBMINER_TEST_CAPTURE" ]; then printf "%s\\n" "$@" > "$SUBMINER_TEST_CAPTURE"; fi\nexit 0\n',
|
`#!/bin/sh\n${RUNTIME_PLUGIN_PREFLIGHT_SH}if [ -n "$SUBMINER_TEST_CAPTURE" ]; then printf "%s\\n" "$@" > "$SUBMINER_TEST_CAPTURE"; fi\nexit 0\n`,
|
||||||
);
|
);
|
||||||
fs.chmodSync(appPath, 0o755);
|
fs.chmodSync(appPath, 0o755);
|
||||||
|
|
||||||
@@ -288,7 +313,7 @@ test('launcher settings command forwards app settings window command', () => {
|
|||||||
const capturePath = path.join(root, 'captured-args.txt');
|
const capturePath = path.join(root, 'captured-args.txt');
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
appPath,
|
appPath,
|
||||||
'#!/bin/sh\nif [ -n "$SUBMINER_TEST_CAPTURE" ]; then printf "%s\\n" "$@" > "$SUBMINER_TEST_CAPTURE"; fi\nexit 0\n',
|
`#!/bin/sh\n${RUNTIME_PLUGIN_PREFLIGHT_SH}if [ -n "$SUBMINER_TEST_CAPTURE" ]; then printf "%s\\n" "$@" > "$SUBMINER_TEST_CAPTURE"; fi\nexit 0\n`,
|
||||||
);
|
);
|
||||||
fs.chmodSync(appPath, 0o755);
|
fs.chmodSync(appPath, 0o755);
|
||||||
|
|
||||||
@@ -369,7 +394,7 @@ test('launcher forwards --args to mpv as parsed tokens', { timeout: 15000 }, ()
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
fs.writeFileSync(appPath, '#!/bin/sh\nexit 0\n');
|
fs.writeFileSync(appPath, `#!/bin/sh\n${RUNTIME_PLUGIN_PREFLIGHT_SH}exit 0\n`);
|
||||||
fs.chmodSync(appPath, 0o755);
|
fs.chmodSync(appPath, 0o755);
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
@@ -460,7 +485,7 @@ test('launcher forwards non-info log level into mpv logging args', { timeout: 15
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
fs.writeFileSync(appPath, '#!/bin/sh\nexit 0\n');
|
fs.writeFileSync(appPath, `#!/bin/sh\n${RUNTIME_PLUGIN_PREFLIGHT_SH}exit 0\n`);
|
||||||
fs.chmodSync(appPath, 0o755);
|
fs.chmodSync(appPath, 0o755);
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
@@ -539,7 +564,7 @@ test('launcher routes youtube urls through regular playback startup', { timeout:
|
|||||||
);
|
);
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
appPath,
|
appPath,
|
||||||
'#!/bin/sh\nif [ -n "$SUBMINER_TEST_CAPTURE" ]; then printf "%s\\n" "$@" > "$SUBMINER_TEST_CAPTURE"; fi\nexit 0\n',
|
`#!/bin/sh\n${RUNTIME_PLUGIN_PREFLIGHT_SH}if [ -n "$SUBMINER_TEST_CAPTURE" ]; then printf "%s\\n" "$@" > "$SUBMINER_TEST_CAPTURE"; fi\nexit 0\n`,
|
||||||
);
|
);
|
||||||
fs.chmodSync(appPath, 0o755);
|
fs.chmodSync(appPath, 0o755);
|
||||||
|
|
||||||
@@ -566,17 +591,25 @@ ${bunBinary} -e "const net=require('node:net'); const fs=require('node:fs'); con
|
|||||||
fs.chmodSync(path.join(binDir, 'yt-dlp'), 0o755);
|
fs.chmodSync(path.join(binDir, 'yt-dlp'), 0o755);
|
||||||
fs.chmodSync(path.join(binDir, 'ffmpeg'), 0o755);
|
fs.chmodSync(path.join(binDir, 'ffmpeg'), 0o755);
|
||||||
|
|
||||||
|
// Note: no SUBMINER_TEST_CAPTURE here. When set, the launcher intercepts
|
||||||
|
// *every* app command — including the Linux runtime-plugin preflight's
|
||||||
|
// `--ensure-linux-runtime-plugin-assets` install — and returns without
|
||||||
|
// running the fake app, so the preflight would poll 30s for a response that
|
||||||
|
// never arrives and time out. This test asserts on the mpv args, not on
|
||||||
|
// captured app args, so capture isn't needed.
|
||||||
const env = {
|
const env = {
|
||||||
...makeTestEnv(homeDir, xdgConfigHome),
|
...makeTestEnv(homeDir, xdgConfigHome),
|
||||||
PATH: `${binDir}${path.delimiter}${process.env.Path || process.env.PATH || ''}`,
|
PATH: `${binDir}${path.delimiter}${process.env.Path || process.env.PATH || ''}`,
|
||||||
Path: `${binDir}${path.delimiter}${process.env.Path || process.env.PATH || ''}`,
|
Path: `${binDir}${path.delimiter}${process.env.Path || process.env.PATH || ''}`,
|
||||||
DISPLAY: ':99',
|
|
||||||
XDG_SESSION_TYPE: 'x11',
|
|
||||||
SUBMINER_APPIMAGE_PATH: appPath,
|
SUBMINER_APPIMAGE_PATH: appPath,
|
||||||
SUBMINER_TEST_MPV_ARGS: mpvArgsPath,
|
SUBMINER_TEST_MPV_ARGS: mpvArgsPath,
|
||||||
SUBMINER_TEST_CAPTURE: path.join(root, 'captured-args.txt'),
|
|
||||||
};
|
};
|
||||||
const result = runLauncher(['https://www.youtube.com/watch?v=abc123'], env);
|
// Pass an explicit backend so overlay startup doesn't probe for a display
|
||||||
|
// (headless CI has none), matching launcher/smoke.e2e.test.ts.
|
||||||
|
const result = runLauncher(
|
||||||
|
['--backend', 'x11', 'https://www.youtube.com/watch?v=abc123'],
|
||||||
|
env,
|
||||||
|
);
|
||||||
|
|
||||||
assert.equal(result.status, 0, `stdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
|
assert.equal(result.status, 0, `stdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
|
||||||
const forwardedArgs = fs
|
const forwardedArgs = fs
|
||||||
@@ -597,7 +630,7 @@ test('dictionary command forwards --dictionary and --dictionary-target to app co
|
|||||||
const capturePath = path.join(root, 'captured-args.txt');
|
const capturePath = path.join(root, 'captured-args.txt');
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
appPath,
|
appPath,
|
||||||
'#!/bin/sh\nif [ -n "$SUBMINER_TEST_CAPTURE" ]; then printf "%s\\n" "$@" > "$SUBMINER_TEST_CAPTURE"; fi\nexit 0\n',
|
`#!/bin/sh\n${RUNTIME_PLUGIN_PREFLIGHT_SH}if [ -n "$SUBMINER_TEST_CAPTURE" ]; then printf "%s\\n" "$@" > "$SUBMINER_TEST_CAPTURE"; fi\nexit 0\n`,
|
||||||
);
|
);
|
||||||
fs.chmodSync(appPath, 0o755);
|
fs.chmodSync(appPath, 0o755);
|
||||||
|
|
||||||
@@ -626,7 +659,7 @@ test('dictionary command forwards manual AniList selection modes to app command
|
|||||||
const capturePath = path.join(root, 'captured-args.txt');
|
const capturePath = path.join(root, 'captured-args.txt');
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
appPath,
|
appPath,
|
||||||
'#!/bin/sh\nif [ -n "$SUBMINER_TEST_CAPTURE" ]; then printf "%s\\n" "$@" > "$SUBMINER_TEST_CAPTURE"; fi\nexit 0\n',
|
`#!/bin/sh\n${RUNTIME_PLUGIN_PREFLIGHT_SH}if [ -n "$SUBMINER_TEST_CAPTURE" ]; then printf "%s\\n" "$@" > "$SUBMINER_TEST_CAPTURE"; fi\nexit 0\n`,
|
||||||
);
|
);
|
||||||
fs.chmodSync(appPath, 0o755);
|
fs.chmodSync(appPath, 0o755);
|
||||||
|
|
||||||
@@ -763,7 +796,7 @@ test('jellyfin discovery routes to app --background and remote announce with log
|
|||||||
const capturePath = path.join(root, 'captured-args.txt');
|
const capturePath = path.join(root, 'captured-args.txt');
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
appPath,
|
appPath,
|
||||||
'#!/bin/sh\nif [ -n "$SUBMINER_TEST_CAPTURE" ]; then printf "%s\\n" "$@" > "$SUBMINER_TEST_CAPTURE"; fi\nexit 0\n',
|
`#!/bin/sh\n${RUNTIME_PLUGIN_PREFLIGHT_SH}if [ -n "$SUBMINER_TEST_CAPTURE" ]; then printf "%s\\n" "$@" > "$SUBMINER_TEST_CAPTURE"; fi\nexit 0\n`,
|
||||||
);
|
);
|
||||||
fs.chmodSync(appPath, 0o755);
|
fs.chmodSync(appPath, 0o755);
|
||||||
|
|
||||||
@@ -790,7 +823,7 @@ test('jellyfin discovery via jf alias forwards remote announce for cast visibili
|
|||||||
const capturePath = path.join(root, 'captured-args.txt');
|
const capturePath = path.join(root, 'captured-args.txt');
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
appPath,
|
appPath,
|
||||||
'#!/bin/sh\nif [ -n "$SUBMINER_TEST_CAPTURE" ]; then printf "%s\\n" "$@" > "$SUBMINER_TEST_CAPTURE"; fi\nexit 0\n',
|
`#!/bin/sh\n${RUNTIME_PLUGIN_PREFLIGHT_SH}if [ -n "$SUBMINER_TEST_CAPTURE" ]; then printf "%s\\n" "$@" > "$SUBMINER_TEST_CAPTURE"; fi\nexit 0\n`,
|
||||||
);
|
);
|
||||||
fs.chmodSync(appPath, 0o755);
|
fs.chmodSync(appPath, 0o755);
|
||||||
|
|
||||||
@@ -817,7 +850,7 @@ test('jellyfin login routes credentials to app command', () => {
|
|||||||
const capturePath = path.join(root, 'captured-args.txt');
|
const capturePath = path.join(root, 'captured-args.txt');
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
appPath,
|
appPath,
|
||||||
'#!/bin/sh\nif [ -n "$SUBMINER_TEST_CAPTURE" ]; then printf "%s\\n" "$@" > "$SUBMINER_TEST_CAPTURE"; fi\nexit 0\n',
|
`#!/bin/sh\n${RUNTIME_PLUGIN_PREFLIGHT_SH}if [ -n "$SUBMINER_TEST_CAPTURE" ]; then printf "%s\\n" "$@" > "$SUBMINER_TEST_CAPTURE"; fi\nexit 0\n`,
|
||||||
);
|
);
|
||||||
fs.chmodSync(appPath, 0o755);
|
fs.chmodSync(appPath, 0o755);
|
||||||
|
|
||||||
@@ -856,7 +889,7 @@ test('jellyfin setup forwards password-store to app command', () => {
|
|||||||
const capturePath = path.join(root, 'captured-args.txt');
|
const capturePath = path.join(root, 'captured-args.txt');
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
appPath,
|
appPath,
|
||||||
'#!/bin/sh\nif [ -n "$SUBMINER_TEST_CAPTURE" ]; then printf "%s\\n" "$@" > "$SUBMINER_TEST_CAPTURE"; fi\nexit 0\n',
|
`#!/bin/sh\n${RUNTIME_PLUGIN_PREFLIGHT_SH}if [ -n "$SUBMINER_TEST_CAPTURE" ]; then printf "%s\\n" "$@" > "$SUBMINER_TEST_CAPTURE"; fi\nexit 0\n`,
|
||||||
);
|
);
|
||||||
fs.chmodSync(appPath, 0o755);
|
fs.chmodSync(appPath, 0o755);
|
||||||
|
|
||||||
|
|||||||
+7
-10
@@ -44,35 +44,32 @@
|
|||||||
"docs:preview": "bun run --cwd docs-site docs:preview",
|
"docs:preview": "bun run --cwd docs-site docs:preview",
|
||||||
"docs:test": "bun run --cwd docs-site test",
|
"docs:test": "bun run --cwd docs-site test",
|
||||||
"test:docs:kb": "bun test scripts/docs-knowledge-base.test.ts",
|
"test:docs:kb": "bun test scripts/docs-knowledge-base.test.ts",
|
||||||
"test:config:src": "bun test src/config/config.test.ts src/config/path-resolution.test.ts src/config/resolve/anki-connect.test.ts src/config/resolve/integrations.test.ts src/config/resolve/subtitle-style.test.ts src/config/resolve/jellyfin.test.ts src/config/definitions/domain-registry.test.ts src/generate-config-example.test.ts src/verify-config-example.test.ts",
|
"test:config:src": "bun scripts/run-test-lane.mjs config",
|
||||||
"test:config:dist": "bun test dist/config/config.test.js dist/config/path-resolution.test.js dist/config/resolve/anki-connect.test.js dist/config/resolve/integrations.test.js dist/config/resolve/subtitle-style.test.js dist/config/resolve/jellyfin.test.js dist/config/definitions/domain-registry.test.js dist/generate-config-example.test.js dist/verify-config-example.test.js",
|
|
||||||
"test:config:smoke:dist": "bun test dist/config/path-resolution.test.js",
|
"test:config:smoke:dist": "bun test dist/config/path-resolution.test.js",
|
||||||
"test:plugin:src": "lua scripts/test-plugin-lua-compat.lua && lua scripts/test-plugin-start-gate.lua && lua scripts/test-plugin-restart-feedback.lua && lua scripts/test-plugin-session-bindings.lua && lua scripts/test-plugin-binary-windows.lua",
|
"test:plugin:src": "lua scripts/test-plugin-lua-compat.lua && lua scripts/test-plugin-start-gate.lua && lua scripts/test-plugin-process-start-retries.lua && lua scripts/test-plugin-restart-feedback.lua && lua scripts/test-plugin-session-bindings.lua && lua scripts/test-plugin-binary-windows.lua",
|
||||||
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
|
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
|
||||||
"test:launcher:src": "bun test launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/mpv.test.ts launcher/picker.test.ts launcher/parse-args.test.ts launcher/history.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/commands/update-command.test.ts launcher/smoke.e2e.test.ts && bun run test:plugin:src",
|
"test:launcher:src": "bun scripts/run-test-lane.mjs launcher && bun run test:plugin:src",
|
||||||
"test:core:src": "bun test src/preload-settings.test.ts src/settings/settings-anki-controls.test.ts src/settings/settings-model.test.ts src/settings/settings-field-layout.test.ts src/cli/args.test.ts src/cli/help.test.ts src/shared/setup-state.test.ts src/shared/mpv-x11-backend.test.ts src/core/services/cli-command.test.ts src/core/services/ipc.test.ts src/core/services/anki-jimaku-ipc.test.ts src/core/services/field-grouping.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/yomitan-extension-paths.test.ts src/core/services/yomitan-extension-loader.test.ts src/core/services/yomitan-settings.test.ts src/core/services/settings-window-z-order.test.ts src/core/services/hyprland-window-placement.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-manager.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/stats-window.test.ts src/core/services/stats-window-lifecycle.test.ts src/core/services/__tests__/stats-server.test.ts src/main/runtime/stats-server-routing.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jimaku-download-path.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/main/overlay-runtime.test.ts src/main/runtime/macos-mpv-focus.test.ts src/main/runtime/macos-modal-focus-handoff.test.ts src/main/runtime/current-subtitle-snapshot.test.ts src/main/runtime/youtube-media-cache-playback.test.ts src/main/runtime/autoplay-ready-gate.test.ts src/main/runtime/autoplay-tokenization-warm-release.test.ts src/main/runtime/autoplay-subtitle-primer.test.ts src/main/runtime/visible-overlay-autoplay-readiness.test.ts src/main/runtime/character-dictionary-manager-gate.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/services/anilist/rate-limiter.test.ts src/core/services/jlpt-token-filter.test.ts src/core/services/subtitle-position.test.ts src/core/utils/shortcut-config.test.ts src/core/utils/electron-backend.test.ts src/core/utils/notification.test.ts src/main/runtime/startup-mode-flags.test.ts src/main/runtime/linux-overlay-pointer-interaction.test.ts src/main/runtime/windows-overlay-pointer-interaction.test.ts src/main/runtime/linux-overlay-zorder-keepalive.test.ts src/main/runtime/config-settings-window.test.ts src/main/runtime/settings-window-z-order.test.ts src/main/runtime/setup-window-factory.test.ts src/main/runtime/first-run-setup-plugin.test.ts src/main/runtime/windows-mpv-plugin-detection.test.ts src/main/runtime/first-run-setup-service.test.ts src/main/runtime/first-run-setup-window.test.ts src/main/runtime/command-line-launcher.test.ts src/main/runtime/cli-command-context.test.ts src/main/runtime/composers/cli-startup-composer.test.ts src/main/runtime/log-export.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts src/main/runtime/cli-command-context-main-deps.test.ts src/main/runtime/app-ready-main-deps.test.ts src/main/runtime/overlay-bootstrap-main-deps.test.ts src/main/runtime/update/appimage-updater.test.ts src/main/runtime/update/fetch-adapter.test.ts src/main/runtime/update/release-metadata-policy.test.ts src/main/runtime/update/update-dialogs.test.ts src/main/runtime/update/support-assets.test.ts src/renderer/error-recovery.test.ts src/renderer/overlay-content-measurement.test.ts src/renderer/subtitle-render.test.ts src/renderer/subtitle-render-word-class.test.ts src/renderer/handlers/mouse.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/modals/jimaku.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/main/character-dictionary-runtime/term-building.test.ts src/window-trackers/hyprland-tracker.test.ts src/window-trackers/x11-tracker.test.ts src/window-trackers/windows-helper.test.ts src/window-trackers/windows-tracker.test.ts src/core/services/overlay-visibility.test.ts src/core/services/overlay-window-config.test.ts src/core/services/overlay-window.test.ts src/maiLine truncated
|
|
||||||
"test:core:dist": "bun test dist/preload-settings.test.js dist/settings/settings-anki-controls.test.js dist/settings/settings-model.test.js dist/settings/settings-field-layout.test.js dist/cli/args.test.js dist/cli/help.test.js dist/shared/setup-state.test.js dist/shared/mpv-x11-backend.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/yomitan-extension-paths.test.js dist/core/services/yomitan-extension-loader.test.js dist/core/services/yomitan-settings.test.js dist/core/services/settings-window-z-order.test.js dist/core/services/hyprland-window-placement.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/stats-window.test.js dist/core/services/stats-window-lifecycle.test.js dist/core/services/__tests__/stats-server.test.js dist/main/runtime/stats-server-routing.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jimaku-download-path.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/overlay-runtime-init.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/main/overlay-runtime.test.js dist/main/runtime/macos-mpv-focus.test.js dist/main/runtime/macos-modal-focus-handoff.test.js dist/main/runtime/current-subtitle-snapshot.test.js dist/main/runtime/youtube-media-cache-playback.test.js dist/main/runtime/autoplay-ready-gate.test.js dist/main/runtime/autoplay-tokenization-warm-release.test.js dist/main/runtime/autoplay-subtitle-primer.test.js dist/main/runtime/visible-overlay-autoplay-readiness.test.js dist/main/runtime/character-dictionary-manager-gate.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/core/services/anilist/rate-limiter.test.js dist/core/services/jlpt-token-filter.test.js dist/core/services/subtitle-position.test.js dist/core/utils/shortcut-config.test.js dist/core/utils/electron-backend.test.js dist/core/utils/notification.test.js dist/main/runtime/startup-mode-flags.test.js dist/main/runtime/linux-overlay-pointer-interaction.test.js dist/main/runtime/windows-overlay-pointer-interaction.test.js dist/main/runtime/linux-overlay-zorder-keepalive.test.js dist/main/runtime/config-settings-window.test.js dist/main/runtime/settings-window-z-order.test.js dist/main/runtime/setup-window-factory.test.js dist/main/runtime/first-run-setup-plugin.test.js dist/main/runtime/windows-mpv-plugin-detection.test.js dist/main/runtime/first-run-setup-service.test.js dist/main/runtime/first-run-setup-window.test.js dist/main/runtime/command-line-launcher.test.js dist/main/runtime/cli-command-context.test.js dist/main/runtime/composers/cli-startup-composer.test.js dist/main/runtime/log-export.test.js dist/main/runtime/tray-runtime.test.js dist/main/runtime/tray-main-actions.test.js dist/main/runtime/tray-main-deps.test.js dist/main/runtime/tray-runtime-handlers.test.js dist/main/runtime/cli-command-context-main-deps.test.js dist/main/runtime/app-ready-main-deps.test.js dist/main/runtime/update/appimage-updater.test.js dist/main/runtime/update/fetch-adapter.test.js dist/main/runtime/update/release-metadata-policy.test.js dist/main/runtime/update/update-dialogs.test.js dist/main/runtime/update/support-assets.test.js dist/renderer/error-recovery.test.js dist/renderer/overlay-content-measurement.test.js dist/renderer/subtitle-render.test.js dist/renderer/subtitle-render-word-class.test.js dist/renderer/handlers/mouse.test.js dist/renderer/handlers/keyboard.test.js dist/renderer/modals/jimaku.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/main/character-dictionary-runtime/term-building.test.js dist/window-trackers/hyprland-tracker.test.js dist/window-trackers/x11-tracker.test.js dist/window-trackers/windows-helper.test.js dist/window-trackers/windows-tracker.test.js dist/core/services/overlay-visibility.test.js dist/core/services/overlay-window-config.test.js dist/core/services/overlay-windowLine truncated
|
|
||||||
"test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
"test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
||||||
"test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist",
|
"test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist",
|
||||||
"test:subtitle:src": "bun test src/core/services/subsync.test.ts src/subsync/utils.test.ts",
|
"test:subtitle:src": "bun test src/core/services/subsync.test.ts src/subsync/utils.test.ts",
|
||||||
"test:immersion:sqlite:src": "bun test src/core/services/immersion-tracker-service.test.ts src/core/services/immersion-tracker/storage-session.test.ts",
|
"test:immersion:sqlite:src": "bun test src/core/services/immersion-tracker-service.test.ts src/core/services/immersion-tracker/storage-session.test.ts",
|
||||||
"test:immersion:sqlite:dist": "bun test dist/core/services/immersion-tracker-service.test.js dist/core/services/immersion-tracker/storage-session.test.js",
|
"test:immersion:sqlite:dist": "bun test dist/core/services/immersion-tracker-service.test.js dist/core/services/immersion-tracker/storage-session.test.js",
|
||||||
"test:immersion:sqlite": "bun run tsc && bun run test:immersion:sqlite:dist",
|
"test:immersion:sqlite": "bun run tsc && bun run test:immersion:sqlite:dist",
|
||||||
"test:src": "bun scripts/run-test-lane.mjs bun-src-full",
|
"test:src": "bun run build:yomitan && bun scripts/run-test-lane.mjs bun-src-full",
|
||||||
"test:coverage:src": "bun run build:yomitan && bun run scripts/run-coverage-lane.ts bun-src-full --coverage-dir coverage/test-src",
|
"test:coverage:src": "bun run build:yomitan && bun run scripts/run-coverage-lane.ts bun-src-full --coverage-dir coverage/test-src",
|
||||||
"test:coverage:subtitle:src": "bun test --coverage --coverage-reporter=text --coverage-reporter=lcov --coverage-dir coverage/test-subtitle src/core/services/subsync.test.ts src/subsync/utils.test.ts",
|
"test:coverage:subtitle:src": "bun test --coverage --coverage-reporter=text --coverage-reporter=lcov --coverage-dir coverage/test-subtitle src/core/services/subsync.test.ts src/subsync/utils.test.ts",
|
||||||
"test:launcher:unit:src": "bun scripts/run-test-lane.mjs bun-launcher-unit",
|
"test:launcher:unit:src": "bun scripts/run-test-lane.mjs bun-launcher-unit",
|
||||||
|
"test:scripts": "bun scripts/run-test-lane.mjs scripts",
|
||||||
|
"test:stats": "bun scripts/run-test-lane.mjs stats",
|
||||||
"test:launcher:env:src": "bun run test:launcher:smoke:src && bun run test:plugin:src",
|
"test:launcher:env:src": "bun run test:launcher:smoke:src && bun run test:plugin:src",
|
||||||
"test:env": "bun run test:launcher:env:src && bun run test:immersion:sqlite:src",
|
"test:env": "bun run test:launcher:env:src && bun run test:immersion:sqlite:src",
|
||||||
"test:runtime:compat": "bun run tsc && bun test dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/overlay-manager.test.js dist/main/config-validation.test.js dist/main/runtime/registry.test.js dist/main/runtime/startup-config.test.js",
|
"test:runtime:compat": "bun run tsc && bun test dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/overlay-manager.test.js dist/main/config-validation.test.js dist/main/runtime/registry.test.js dist/main/runtime/startup-config.test.js",
|
||||||
"test:node:compat": "bun run test:runtime:compat",
|
"test:node:compat": "bun run test:runtime:compat",
|
||||||
"test:full": "bun run test:src && bun run test:launcher:unit:src && bun run test:node:compat",
|
|
||||||
"test": "bun run test:fast",
|
"test": "bun run test:fast",
|
||||||
"test:config": "bun run test:config:src",
|
"test:config": "bun run test:config:src",
|
||||||
"test:launcher": "bun run test:launcher:src",
|
"test:launcher": "bun run test:launcher:src",
|
||||||
"test:core": "bun run test:core:src",
|
|
||||||
"test:subtitle": "bun run test:subtitle:src",
|
"test:subtitle": "bun run test:subtitle:src",
|
||||||
"test:fast": "bun run test:config:src && bun run test:core:src && bun run test:docs:kb && bun test src/main-entry-runtime.test.ts src/anki-integration.test.ts src/anki-integration/card-creation-manual-update.test.ts src/anki-integration/card-creation-sentence-media.test.ts src/anki-integration/anki-connect-proxy.test.ts src/anki-integration/field-grouping-workflow.test.ts src/anki-integration/field-grouping.test.ts src/anki-integration/field-grouping-merge.test.ts src/release-workflow.test.ts src/prerelease-workflow.test.ts src/ci-workflow.test.ts scripts/docs-versioning.test.ts scripts/docs-versioned-assets.test.ts scripts/build-changelog.test.ts scripts/electron-builder-after-pack.test.ts scripts/get-mpv-window-macos.test.ts scripts/prepare-build-assets.test.ts scripts/mkv-to-readme-video.test.ts scripts/run-coverage-lane.test.ts scripts/update-aur-package.test.ts && bun test src/core/services/immersion-tracker/__tests__/query.test.ts src/core/services/immersion-tracker/__tests__/query-split-modules.test.ts && bun run tsc && bun test dist/main/runtime/registry.test.js",
|
"test:fast": "bun run test:src && bun run test:launcher:unit:src && bun run test:scripts && bun run test:runtime:compat",
|
||||||
"generate:config-example": "bun run src/generate-config-example.ts",
|
"generate:config-example": "bun run src/generate-config-example.ts",
|
||||||
"verify:config-example": "bun run src/verify-config-example.ts",
|
"verify:config-example": "bun run src/verify-config-example.ts",
|
||||||
"start": "bun run build && electron . --start",
|
"start": "bun run build && electron . --start",
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ bash plugins/subminer-workflow/skills/subminer-change-verification/scripts/verif
|
|||||||
- For `docs-site/`, `docs/`, and doc-only edits.
|
- For `docs-site/`, `docs/`, and doc-only edits.
|
||||||
- `config`
|
- `config`
|
||||||
- For `src/config/` and config-template-sensitive edits.
|
- For `src/config/` and config-template-sensitive edits.
|
||||||
|
- `stats`
|
||||||
|
- For `stats/` dashboard UI edits.
|
||||||
- `core`
|
- `core`
|
||||||
- For general source changes where `typecheck` + `test:fast` is the best cheap signal.
|
- For general source changes where `typecheck` + `test:fast` is the best cheap signal.
|
||||||
- `launcher-plugin`
|
- `launcher-plugin`
|
||||||
|
|||||||
+8
@@ -108,6 +108,14 @@ for path in "${PATHS[@]}"; do
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
case "$path" in
|
||||||
|
stats/*)
|
||||||
|
add_lane "stats"
|
||||||
|
add_reason "$path -> stats"
|
||||||
|
specialized=1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
case "$path" in
|
case "$path" in
|
||||||
launcher/*|plugin/subminer/*|plugin/subminer.conf|scripts/test-plugin-*|scripts/get-mpv-window-*|scripts/configure-plugin-binary-path.mjs)
|
launcher/*|plugin/subminer/*|plugin/subminer.conf|scripts/test-plugin-*|scripts/get-mpv-window-*|scripts/configure-plugin-binary-path.mjs)
|
||||||
add_lane "launcher-plugin"
|
add_lane "launcher-plugin"
|
||||||
|
|||||||
+15
-2
@@ -255,8 +255,18 @@ write_summary_files() {
|
|||||||
local lane_lines
|
local lane_lines
|
||||||
lane_lines=$(printf '%s\n' "${SELECTED_LANES[@]}")
|
lane_lines=$(printf '%s\n' "${SELECTED_LANES[@]}")
|
||||||
printf '%s\n' "$lane_lines" >"$ARTIFACT_DIR/lanes.txt"
|
printf '%s\n' "$lane_lines" >"$ARTIFACT_DIR/lanes.txt"
|
||||||
printf '%s\n' "${BLOCKERS[@]}" >"$ARTIFACT_DIR/blockers.txt"
|
# bash 3.2 raises "unbound variable" under set -u when expanding an empty
|
||||||
printf '%s\n' "${PATH_ARGS[@]}" >"$ARTIFACT_DIR/requested-paths.txt"
|
# array, so guard on length (matching the idiom used elsewhere here).
|
||||||
|
if [[ ${#BLOCKERS[@]} -gt 0 ]]; then
|
||||||
|
printf '%s\n' "${BLOCKERS[@]}" >"$ARTIFACT_DIR/blockers.txt"
|
||||||
|
else
|
||||||
|
: >"$ARTIFACT_DIR/blockers.txt"
|
||||||
|
fi
|
||||||
|
if [[ ${#PATH_ARGS[@]} -gt 0 ]]; then
|
||||||
|
printf '%s\n' "${PATH_ARGS[@]}" >"$ARTIFACT_DIR/requested-paths.txt"
|
||||||
|
else
|
||||||
|
: >"$ARTIFACT_DIR/requested-paths.txt"
|
||||||
|
fi
|
||||||
|
|
||||||
ARTIFACT_DIR_ENV="$ARTIFACT_DIR" \
|
ARTIFACT_DIR_ENV="$ARTIFACT_DIR" \
|
||||||
SESSION_ID_ENV="$SESSION_ID" \
|
SESSION_ID_ENV="$SESSION_ID" \
|
||||||
@@ -482,6 +492,9 @@ for lane in "${SELECTED_LANES[@]}"; do
|
|||||||
config)
|
config)
|
||||||
run_step "$lane" "config" "bun run test:config" || break
|
run_step "$lane" "config" "bun run test:config" || break
|
||||||
;;
|
;;
|
||||||
|
stats)
|
||||||
|
run_step "$lane" "stats" "bun run test:stats" || break
|
||||||
|
;;
|
||||||
core)
|
core)
|
||||||
run_step "$lane" "typecheck" "bun run typecheck" || break
|
run_step "$lane" "typecheck" "bun run typecheck" || break
|
||||||
run_step "$lane" "fast-tests" "bun run test:fast" || break
|
run_step "$lane" "fast-tests" "bun run test:fast" || break
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
import { spawnSync } from 'node:child_process';
|
import { spawnSync } from 'node:child_process';
|
||||||
import { isAbsolute, join, relative, resolve } from 'node:path';
|
import { isAbsolute, join, relative, resolve } from 'node:path';
|
||||||
|
import { collectLaneFiles } from './test-lanes';
|
||||||
type LaneConfig = {
|
|
||||||
roots: string[];
|
|
||||||
include: string[];
|
|
||||||
exclude: Set<string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type LcovRecord = {
|
type LcovRecord = {
|
||||||
sourceFile: string;
|
sourceFile: string;
|
||||||
@@ -18,64 +13,6 @@ type LcovRecord = {
|
|||||||
|
|
||||||
const repoRoot = resolve(__dirname, '..');
|
const repoRoot = resolve(__dirname, '..');
|
||||||
|
|
||||||
const lanes: Record<string, LaneConfig> = {
|
|
||||||
'bun-src-full': {
|
|
||||||
roots: ['src'],
|
|
||||||
include: ['.test.ts', '.type-test.ts'],
|
|
||||||
exclude: new Set([
|
|
||||||
'src/core/services/anki-jimaku-ipc.test.ts',
|
|
||||||
'src/core/services/ipc.test.ts',
|
|
||||||
'src/core/services/overlay-manager.test.ts',
|
|
||||||
'src/main/config-validation.test.ts',
|
|
||||||
'src/main/runtime/registry.test.ts',
|
|
||||||
'src/main/runtime/startup-config.test.ts',
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
'bun-launcher-unit': {
|
|
||||||
roots: ['launcher'],
|
|
||||||
include: ['.test.ts'],
|
|
||||||
exclude: new Set(['launcher/smoke.e2e.test.ts']),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function collectFiles(
|
|
||||||
rootDir: string,
|
|
||||||
includeSuffixes: string[],
|
|
||||||
excludeSet: Set<string>,
|
|
||||||
): string[] {
|
|
||||||
const out: string[] = [];
|
|
||||||
const visit = (currentDir: string) => {
|
|
||||||
for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
|
|
||||||
const fullPath = resolve(currentDir, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
visit(fullPath);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const relPath = relative(repoRoot, fullPath).replaceAll('\\', '/');
|
|
||||||
if (excludeSet.has(relPath)) continue;
|
|
||||||
if (includeSuffixes.some((suffix) => relPath.endsWith(suffix))) {
|
|
||||||
out.push(relPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
visit(resolve(repoRoot, rootDir));
|
|
||||||
out.sort();
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLaneFiles(laneName: string): string[] {
|
|
||||||
const lane = lanes[laneName];
|
|
||||||
if (!lane) {
|
|
||||||
throw new Error(`Unknown coverage lane: ${laneName}`);
|
|
||||||
}
|
|
||||||
const files = lane.roots.flatMap((rootDir) => collectFiles(rootDir, lane.include, lane.exclude));
|
|
||||||
if (files.length === 0) {
|
|
||||||
throw new Error(`No test files found for coverage lane: ${laneName}`);
|
|
||||||
}
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseCoverageDirArg(argv: string[]): string {
|
function parseCoverageDirArg(argv: string[]): string {
|
||||||
for (let index = 0; index < argv.length; index += 1) {
|
for (let index = 0; index < argv.length; index += 1) {
|
||||||
if (argv[index] === '--coverage-dir') {
|
if (argv[index] === '--coverage-dir') {
|
||||||
@@ -277,7 +214,13 @@ function runCoverageLane(): number {
|
|||||||
rmSync(shardRoot, { recursive: true, force: true });
|
rmSync(shardRoot, { recursive: true, force: true });
|
||||||
mkdirSync(shardRoot, { recursive: true });
|
mkdirSync(shardRoot, { recursive: true });
|
||||||
|
|
||||||
const files = getLaneFiles(laneName);
|
let files: string[];
|
||||||
|
try {
|
||||||
|
files = collectLaneFiles(repoRoot, laneName);
|
||||||
|
} catch (error) {
|
||||||
|
process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
const reports: string[] = [];
|
const reports: string[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
+133
-53
@@ -1,73 +1,153 @@
|
|||||||
import { readdirSync } from 'node:fs';
|
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { relative, resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
import { spawnSync } from 'node:child_process';
|
import { spawn, spawnSync } from 'node:child_process';
|
||||||
|
import { collectLaneFiles } from './test-lanes.ts';
|
||||||
|
|
||||||
|
// Runs a test lane with per-file process isolation: one `bun test` process per
|
||||||
|
// test file so a hanging test or leaked global in one file cannot poison the
|
||||||
|
// rest of the lane. Use --single-process for the old all-in-one-process mode.
|
||||||
|
//
|
||||||
|
// Usage: bun scripts/run-test-lane.mjs <lane> [--jobs N] [--timeout-secs N] [--single-process]
|
||||||
|
|
||||||
const repoRoot = resolve(fileURLToPath(new URL('..', import.meta.url)));
|
const repoRoot = resolve(fileURLToPath(new URL('..', import.meta.url)));
|
||||||
|
|
||||||
const lanes = {
|
// Cap per-file buffered output so a long or noisy test cannot grow the string
|
||||||
'bun-src-full': {
|
// without bound and exhaust memory.
|
||||||
roots: ['src'],
|
const MAX_OUTPUT_BYTES = 1024 * 1024;
|
||||||
include: ['.test.ts', '.type-test.ts'],
|
|
||||||
exclude: new Set([
|
|
||||||
'src/core/services/anki-jimaku-ipc.test.ts',
|
|
||||||
'src/core/services/ipc.test.ts',
|
|
||||||
'src/core/services/overlay-manager.test.ts',
|
|
||||||
'src/main/config-validation.test.ts',
|
|
||||||
'src/main/runtime/registry.test.ts',
|
|
||||||
'src/main/runtime/startup-config.test.ts',
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
'bun-launcher-unit': {
|
|
||||||
roots: ['launcher'],
|
|
||||||
include: ['.test.ts'],
|
|
||||||
exclude: new Set(['launcher/smoke.e2e.test.ts']),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function collectFiles(rootDir, includeSuffixes, excludeSet) {
|
// Track spawned `bun test` children so we can kill them if the runner is
|
||||||
const out = [];
|
// interrupted, avoiding orphaned in-flight test processes.
|
||||||
const visit = (currentDir) => {
|
const activeChildren = new Set();
|
||||||
for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
|
|
||||||
const fullPath = resolve(currentDir, entry.name);
|
function terminateChildren() {
|
||||||
if (entry.isDirectory()) {
|
for (const child of activeChildren) {
|
||||||
visit(fullPath);
|
child.kill('SIGKILL');
|
||||||
continue;
|
}
|
||||||
|
activeChildren.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const signal of ['SIGINT', 'SIGTERM']) {
|
||||||
|
process.on(signal, () => {
|
||||||
|
terminateChildren();
|
||||||
|
process.exit(130);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const options = { lane: undefined, jobs: 1, timeoutSecs: 300, singleProcess: false };
|
||||||
|
for (let index = 0; index < argv.length; index += 1) {
|
||||||
|
const arg = argv[index];
|
||||||
|
if (arg === '--jobs') {
|
||||||
|
options.jobs = Math.max(1, Number(argv[(index += 1)]) || 1);
|
||||||
|
} else if (arg === '--timeout-secs') {
|
||||||
|
options.timeoutSecs = Math.max(1, Number(argv[(index += 1)]) || 300);
|
||||||
|
} else if (arg === '--single-process') {
|
||||||
|
options.singleProcess = true;
|
||||||
|
} else if (!arg.startsWith('--') && options.lane === undefined) {
|
||||||
|
options.lane = arg;
|
||||||
|
} else {
|
||||||
|
process.stderr.write(`Unknown argument: ${arg}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runFile(file, timeoutSecs) {
|
||||||
|
return new Promise((resolvePromise) => {
|
||||||
|
const child = spawn('bun', ['test', `./${file}`], { cwd: repoRoot });
|
||||||
|
activeChildren.add(child);
|
||||||
|
let output = '';
|
||||||
|
let truncated = false;
|
||||||
|
let timedOut = false;
|
||||||
|
const append = (chunk) => {
|
||||||
|
if (truncated) return;
|
||||||
|
output += chunk;
|
||||||
|
if (output.length > MAX_OUTPUT_BYTES) {
|
||||||
|
output = `${output.slice(0, MAX_OUTPUT_BYTES)}\n[output truncated at ${MAX_OUTPUT_BYTES} bytes]\n`;
|
||||||
|
truncated = true;
|
||||||
}
|
}
|
||||||
const relPath = relative(repoRoot, fullPath).replaceAll('\\', '/');
|
};
|
||||||
if (excludeSet.has(relPath)) continue;
|
child.stdout.on('data', append);
|
||||||
if (includeSuffixes.some((suffix) => relPath.endsWith(suffix))) {
|
child.stderr.on('data', append);
|
||||||
out.push(relPath);
|
const timer = setTimeout(() => {
|
||||||
|
timedOut = true;
|
||||||
|
child.kill('SIGKILL');
|
||||||
|
}, timeoutSecs * 1000);
|
||||||
|
child.on('close', (code) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
activeChildren.delete(child);
|
||||||
|
resolvePromise({ file, code: timedOut ? 124 : (code ?? 1), output, timedOut });
|
||||||
|
});
|
||||||
|
child.on('error', (error) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
activeChildren.delete(child);
|
||||||
|
resolvePromise({ file, code: 1, output: String(error), timedOut: false });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runIsolated(files, options) {
|
||||||
|
const failures = [];
|
||||||
|
let nextIndex = 0;
|
||||||
|
let completed = 0;
|
||||||
|
|
||||||
|
async function worker() {
|
||||||
|
while (nextIndex < files.length) {
|
||||||
|
const file = files[nextIndex];
|
||||||
|
nextIndex += 1;
|
||||||
|
const result = await runFile(file, options.timeoutSecs);
|
||||||
|
completed += 1;
|
||||||
|
if (result.code !== 0) {
|
||||||
|
failures.push(result);
|
||||||
|
const reason = result.timedOut ? `timed out after ${options.timeoutSecs}s` : 'failed';
|
||||||
|
process.stderr.write(`\n[${completed}/${files.length}] ${file} ${reason}\n`);
|
||||||
|
process.stderr.write(result.output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
visit(resolve(repoRoot, rootDir));
|
await Promise.all(Array.from({ length: Math.min(options.jobs, files.length) }, worker));
|
||||||
out.sort();
|
|
||||||
return out;
|
if (failures.length > 0) {
|
||||||
|
process.stderr.write(`\n${failures.length} of ${files.length} test files failed:\n`);
|
||||||
|
for (const failure of failures) {
|
||||||
|
process.stderr.write(` ${failure.file}${failure.timedOut ? ' (timeout)' : ''}\n`);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
process.stdout.write(`All ${files.length} test files passed.\n`);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lane = lanes[process.argv[2]];
|
function runSingleProcess(files) {
|
||||||
|
const result = spawnSync('bun', ['test', ...files.map((file) => `./${file}`)], {
|
||||||
|
cwd: repoRoot,
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
if (result.error) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
return result.status ?? 1;
|
||||||
|
}
|
||||||
|
|
||||||
if (!lane) {
|
const options = parseArgs(process.argv.slice(2));
|
||||||
process.stderr.write(`Unknown test lane: ${process.argv[2] ?? '(missing)'}\n`);
|
|
||||||
|
if (!options.lane) {
|
||||||
|
process.stderr.write('Missing test lane name\n');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = lane.roots.flatMap((rootDir) => collectFiles(rootDir, lane.include, lane.exclude));
|
let files;
|
||||||
|
try {
|
||||||
if (files.length === 0) {
|
files = collectLaneFiles(repoRoot, options.lane);
|
||||||
process.stderr.write(`No test files found for lane: ${process.argv[2]}\n`);
|
} catch (error) {
|
||||||
|
process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = spawnSync('bun', ['test', ...files.map((file) => `./${file}`)], {
|
if (options.singleProcess) {
|
||||||
cwd: repoRoot,
|
process.exit(runSingleProcess(files));
|
||||||
stdio: 'inherit',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
throw result.error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
process.exit(result.status ?? 1);
|
process.exit(await runIsolated(files, options));
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { readdirSync } from 'node:fs';
|
||||||
|
import { relative, resolve } from 'node:path';
|
||||||
|
|
||||||
|
export type TestLane = {
|
||||||
|
roots: string[];
|
||||||
|
include: string[];
|
||||||
|
exclude?: string[];
|
||||||
|
extraFiles?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Single source of truth for test-lane membership. Consumed by
|
||||||
|
// scripts/run-test-lane.mjs (plain runs) and scripts/run-coverage-lane.ts
|
||||||
|
// (per-file coverage shards). Lanes discover files by directory so new test
|
||||||
|
// files join their lane automatically.
|
||||||
|
export const testLanes: Record<string, TestLane> = {
|
||||||
|
'bun-src-full': {
|
||||||
|
roots: ['src'],
|
||||||
|
include: ['.test.ts', '.type-test.ts'],
|
||||||
|
// Node-compat suites; their dist builds run via test:runtime:compat.
|
||||||
|
exclude: [
|
||||||
|
'src/core/services/anki-jimaku-ipc.test.ts',
|
||||||
|
'src/core/services/ipc.test.ts',
|
||||||
|
'src/core/services/overlay-manager.test.ts',
|
||||||
|
'src/main/config-validation.test.ts',
|
||||||
|
'src/main/runtime/registry.test.ts',
|
||||||
|
'src/main/runtime/startup-config.test.ts',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
roots: ['src/config'],
|
||||||
|
include: ['.test.ts'],
|
||||||
|
extraFiles: ['src/generate-config-example.test.ts', 'src/verify-config-example.test.ts'],
|
||||||
|
},
|
||||||
|
launcher: {
|
||||||
|
roots: ['launcher'],
|
||||||
|
include: ['.test.ts'],
|
||||||
|
},
|
||||||
|
'bun-launcher-unit': {
|
||||||
|
roots: ['launcher'],
|
||||||
|
include: ['.test.ts'],
|
||||||
|
exclude: ['launcher/smoke.e2e.test.ts'],
|
||||||
|
},
|
||||||
|
scripts: {
|
||||||
|
roots: ['scripts'],
|
||||||
|
include: ['.test.ts'],
|
||||||
|
},
|
||||||
|
stats: {
|
||||||
|
roots: ['stats/src'],
|
||||||
|
include: ['.test.ts', '.test.tsx'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function collectFiles(
|
||||||
|
repoRoot: string,
|
||||||
|
rootDir: string,
|
||||||
|
includeSuffixes: string[],
|
||||||
|
excludeSet: Set<string>,
|
||||||
|
): string[] {
|
||||||
|
const out: string[] = [];
|
||||||
|
const visit = (currentDir: string): void => {
|
||||||
|
for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
|
||||||
|
const fullPath = resolve(currentDir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
visit(fullPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const relPath = relative(repoRoot, fullPath).replaceAll('\\', '/');
|
||||||
|
if (excludeSet.has(relPath)) continue;
|
||||||
|
if (includeSuffixes.some((suffix) => relPath.endsWith(suffix))) {
|
||||||
|
out.push(relPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
visit(resolve(repoRoot, rootDir));
|
||||||
|
out.sort();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectLaneFiles(repoRoot: string, laneName: string): string[] {
|
||||||
|
const lane = testLanes[laneName];
|
||||||
|
if (!lane) {
|
||||||
|
throw new Error(`Unknown test lane: ${laneName}`);
|
||||||
|
}
|
||||||
|
const excludeSet = new Set(lane.exclude ?? []);
|
||||||
|
const files = lane.roots.flatMap((rootDir) =>
|
||||||
|
collectFiles(repoRoot, rootDir, lane.include, excludeSet),
|
||||||
|
);
|
||||||
|
for (const extra of lane.extraFiles ?? []) {
|
||||||
|
if (!files.includes(extra)) files.push(extra);
|
||||||
|
}
|
||||||
|
files.sort();
|
||||||
|
if (files.length === 0) {
|
||||||
|
throw new Error(`No test files found for lane: ${laneName}`);
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { appendClipboardVideoToQueueRuntime } from './clipboard-queue';
|
import { appendClipboardVideoToQueueRuntime } from './clipboard-queue';
|
||||||
|
|
||||||
@@ -27,7 +28,8 @@ test('appendClipboardVideoToQueueRuntime rejects unsupported clipboard path', ()
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('appendClipboardVideoToQueueRuntime queues readable media file', () => {
|
test('appendClipboardVideoToQueueRuntime queues readable media file', () => {
|
||||||
const tempPath = path.join(process.cwd(), 'dist', 'clipboard-queue-test-video.mkv');
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'clipboard-queue-'));
|
||||||
|
const tempPath = path.join(tempDir, 'clipboard-queue-test-video.mkv');
|
||||||
fs.writeFileSync(tempPath, 'stub');
|
fs.writeFileSync(tempPath, 'stub');
|
||||||
|
|
||||||
const commands: Array<(string | number)[]> = [];
|
const commands: Array<(string | number)[]> = [];
|
||||||
@@ -43,5 +45,5 @@ test('appendClipboardVideoToQueueRuntime queues readable media file', () => {
|
|||||||
assert.deepEqual(commands[0], ['loadfile', tempPath, 'append']);
|
assert.deepEqual(commands[0], ['loadfile', tempPath, 'append']);
|
||||||
assert.equal(osdMessages[0], `Queued from clipboard: ${path.basename(tempPath)}`);
|
assert.equal(osdMessages[0], `Queued from clipboard: ${path.basename(tempPath)}`);
|
||||||
|
|
||||||
fs.unlinkSync(tempPath);
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user