@torrent-tv/proxy 2.89.17 → 2.89.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/README.md +14 -3
  2. package/package.json +10 -2
  3. package/server.js +0 -11
  4. package/utils/client-log-file.js +147 -42
  5. package/.claude/commands/opsx/apply.md +0 -155
  6. package/.claude/commands/opsx/archive.md +0 -160
  7. package/.claude/commands/opsx/explore.md +0 -174
  8. package/.claude/commands/opsx/propose.md +0 -109
  9. package/.claude/commands/opsx/sync.md +0 -143
  10. package/.claude/skills/openspec-apply-change/SKILL.md +0 -159
  11. package/.claude/skills/openspec-archive-change/SKILL.md +0 -117
  12. package/.claude/skills/openspec-explore/SKILL.md +0 -289
  13. package/.claude/skills/openspec-propose/SKILL.md +0 -113
  14. package/.claude/skills/openspec-sync-specs/SKILL.md +0 -147
  15. package/.dockerignore +0 -5
  16. package/AGENTS.md +0 -363
  17. package/CHANGELOG.md +0 -2377
  18. package/Dockerfile +0 -30
  19. package/biome.json +0 -412
  20. package/corpus.tmp.json +0 -1
  21. package/docs/browser-proxy-contract.md +0 -155
  22. package/docs/container-architecture.md +0 -273
  23. package/docs/download-architecture.md +0 -261
  24. package/docs/encode-architecture.md +0 -765
  25. package/docs/encode-run-state.md +0 -96
  26. package/docs/logs.md +0 -151
  27. package/docs/playback-start-forecast.md +0 -113
  28. package/docs/socks5-egress-research.md +0 -114
  29. package/docs/storage-architecture.md +0 -273
  30. package/knip.json +0 -14
  31. package/main2.cpuprofile +0 -1
  32. package/openspec/changes/adaptive-bitrate/.openspec.yaml +0 -0
  33. package/openspec/changes/adaptive-bitrate/design.md +0 -92
  34. package/openspec/changes/adaptive-bitrate/proposal.md +0 -56
  35. package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +0 -51
  36. package/openspec/changes/adaptive-bitrate/tasks.md +0 -40
  37. package/openspec/changes/binary-distribution/.openspec.yaml +0 -2
  38. package/openspec/changes/binary-distribution/proposal.md +0 -53
  39. package/openspec/changes/chunked-request-bodies/design.md +0 -81
  40. package/openspec/changes/chunked-request-bodies/proposal.md +0 -53
  41. package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +0 -30
  42. package/openspec/changes/chunked-request-bodies/tasks.md +0 -40
  43. package/openspec/changes/cold-start/design.md +0 -98
  44. package/openspec/changes/cold-start/proposal.md +0 -62
  45. package/openspec/changes/cold-start/specs/cold-start/spec.md +0 -47
  46. package/openspec/changes/cold-start/tasks.md +0 -48
  47. package/openspec/changes/disk-cap/.openspec.yaml +0 -2
  48. package/openspec/changes/disk-cap/proposal.md +0 -37
  49. package/openspec/changes/disk-cap/specs/disk-cap/spec.md +0 -25
  50. package/openspec/changes/disk-cap/tasks.md +0 -19
  51. package/openspec/changes/proxy-observability/.openspec.yaml +0 -2
  52. package/openspec/changes/proxy-observability/design.md +0 -38
  53. package/openspec/changes/proxy-observability/proposal.md +0 -49
  54. package/openspec/changes/proxy-observability/specs/observability/spec.md +0 -33
  55. package/openspec/changes/proxy-observability/tasks.md +0 -21
  56. package/openspec/changes/seek-debounce/.openspec.yaml +0 -0
  57. package/openspec/changes/seek-debounce/design.md +0 -118
  58. package/openspec/changes/seek-debounce/proposal.md +0 -59
  59. package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +0 -41
  60. package/openspec/changes/seek-debounce/tasks.md +0 -33
  61. package/openspec/changes/subtitle-language/.openspec.yaml +0 -2
  62. package/openspec/changes/subtitle-language/proposal.md +0 -50
  63. package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +0 -34
  64. package/openspec/changes/subtitle-language/tasks.md +0 -26
  65. package/openspec/changes/track-selection/.openspec.yaml +0 -2
  66. package/openspec/changes/track-selection/design.md +0 -47
  67. package/openspec/changes/track-selection/proposal.md +0 -45
  68. package/openspec/changes/track-selection/specs/track-selection/spec.md +0 -41
  69. package/openspec/changes/track-selection/tasks.md +0 -20
  70. package/openspec/changes/transcode-quality/.openspec.yaml +0 -2
  71. package/openspec/changes/transcode-quality/proposal.md +0 -51
  72. package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +0 -139
  73. package/openspec/changes/transcode-quality/tasks.md +0 -63
  74. package/openspec/config.yaml +0 -38
  75. package/public/.well-known/appspecific/com.chrome.devtools.json +0 -1
  76. package/research/delivery-wedge-stand-ladder-2026-09-13.md +0 -329
  77. package/research/double-spawn-2026-09-10.md +0 -171
  78. package/research/handover-2026-09-13.md +0 -209
  79. package/research/handover-reader-claims-removal-2026-09-12.md +0 -166
  80. package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +0 -175
  81. package/research/preset-benchmark-size-scaling-2026-08-22.md +0 -55
  82. package/research/priority-map-is-the-truth-2026-09-12.md +0 -225
  83. package/research/valgrind-replica-2026-08-27/logs.tar.gz +0 -0
  84. package/scripts/build-audio-samples.mjs +0 -31
  85. package/scripts/render-run-graph.js +0 -173
  86. package/test/asset-paths.test.js +0 -28
  87. package/test/audio-buffer-continuity.test.js +0 -77
  88. package/test/audio-calibration.test.js +0 -23
  89. package/test/audio-inventory.test.js +0 -212
  90. package/test/audio-track-bitrate.test.js +0 -40
  91. package/test/audio-warm-recorded.test.js +0 -63
  92. package/test/auto-quality-step.test.js +0 -762
  93. package/test/available-share.test.js +0 -76
  94. package/test/background-fill.test.js +0 -190
  95. package/test/client-log-file.test.js +0 -126
  96. package/test/client-logs-route.test.js +0 -34
  97. package/test/coalesce.test.js +0 -39
  98. package/test/completed-files.test.js +0 -115
  99. package/test/concurrent-cost.test.js +0 -148
  100. package/test/container-media-info.test.js +0 -228
  101. package/test/contention.test.js +0 -76
  102. package/test/core-dumps.test.js +0 -57
  103. package/test/coverage-follows-the-disk.test.js +0 -233
  104. package/test/coverage-map.test.js +0 -195
  105. package/test/cue-push-carries-what-the-walk-found.test.js +0 -58
  106. package/test/cut-times-timeline.test.js +0 -65
  107. package/test/cuts-follow-published-grid.test.js +0 -109
  108. package/test/data-channel-frame.test.js +0 -63
  109. package/test/data-channel-viewer-lifecycle.test.js +0 -193
  110. package/test/dead-torrent-handle.test.js +0 -52
  111. package/test/declared-rates.test.js +0 -116
  112. package/test/declared-tracks.test.js +0 -43
  113. package/test/decode-cost-fit.test.js +0 -202
  114. package/test/decode-cost.test.js +0 -475
  115. package/test/delivery-probe-field.test.js +0 -173
  116. package/test/delivery-probe.test.js +0 -382
  117. package/test/demand-register.test.js +0 -206
  118. package/test/dht-bootstrap.test.js +0 -72
  119. package/test/drain-threat.test.js +0 -65
  120. package/test/duplicate-add.test.js +0 -48
  121. package/test/encode-admission.test.js +0 -170
  122. package/test/encode-cost.test.js +0 -275
  123. package/test/encode-exit.test.js +0 -111
  124. package/test/encode-orchestrator.test.js +0 -426
  125. package/test/encode-plan-viewers.test.js +0 -725
  126. package/test/encode-plan.test.js +0 -545
  127. package/test/encode-run-state.test.js +0 -314
  128. package/test/encode-run.test.js +0 -248
  129. package/test/encode-slope.test.js +0 -46
  130. package/test/encoded-output.test.js +0 -39
  131. package/test/encoder-kinds.test.js +0 -122
  132. package/test/encoder-readings.test.js +0 -68
  133. package/test/episode-naming.test.js +0 -131
  134. package/test/every-claimant-is-told-its-share.test.js +0 -152
  135. package/test/failed-encoder-still-serves-what-exists.test.js +0 -354
  136. package/test/failed-start-breaker.test.js +0 -202
  137. package/test/fastest-wires.test.js +0 -143
  138. package/test/ffmpeg-stream-counts.test.js +0 -101
  139. package/test/file-edges.test.js +0 -191
  140. package/test/files-in-use.test.js +0 -34
  141. package/test/fixtures/audio-frame-boundary.json +0 -1
  142. package/test/fixtures/refused-audio-20261002.json +0 -10
  143. package/test/fmp4-presentation-clock.test.js +0 -148
  144. package/test/fmp4-self-contained.test.js +0 -80
  145. package/test/fmp4-timestamps.test.js +0 -140
  146. package/test/fmp4-track-completeness.test.js +0 -41
  147. package/test/format-keyed-outputs.test.js +0 -78
  148. package/test/health-metrics.test.js +0 -36
  149. package/test/held-read-ownership.test.js +0 -77
  150. package/test/held-request-width.test.js +0 -157
  151. package/test/helpers/encode-run.js +0 -248
  152. package/test/helpers/held-file.js +0 -77
  153. package/test/helpers/manager.js +0 -62
  154. package/test/helpers/matroska-file.js +0 -296
  155. package/test/helpers/output-spec.js +0 -63
  156. package/test/host-load.test.js +0 -68
  157. package/test/index-accuracy-is-about-the-file.test.js +0 -128
  158. package/test/init-compat.test.js +0 -219
  159. package/test/input-lost-quiets-the-plan.test.js +0 -261
  160. package/test/input-unavailable.test.js +0 -48
  161. package/test/installation-id.test.js +0 -39
  162. package/test/keeping-period.test.js +0 -83
  163. package/test/keyframe-index-accuracy.test.js +0 -188
  164. package/test/keyframe-table-budget.test.js +0 -123
  165. package/test/keyframes-belong-to-the-file.test.js +0 -249
  166. package/test/learned-median.test.js +0 -37
  167. package/test/letting-go-asks-who-is-watching.test.js +0 -191
  168. package/test/limit-row-by-area.test.js +0 -60
  169. package/test/link-budget.test.js +0 -111
  170. package/test/link-probe.test.js +0 -56
  171. package/test/local-encoding-data.test.js +0 -273
  172. package/test/logger-repeats.test.js +0 -134
  173. package/test/look-ahead-coverage.test.js +0 -56
  174. package/test/manager-surface.test.js +0 -291
  175. package/test/matroska-blocks.test.js +0 -0
  176. package/test/matroska-cluster-discovery.test.js +0 -323
  177. package/test/matroska-cues-track.test.js +0 -192
  178. package/test/media-time.test.js +0 -62
  179. package/test/memory-budget.test.js +0 -390
  180. package/test/move-cost.test.js +0 -216
  181. package/test/mp4-composition-times.test.js +0 -0
  182. package/test/mp4-subtitles.test.js +0 -173
  183. package/test/one-authority.test.js +0 -285
  184. package/test/one-budget.test.js +0 -149
  185. package/test/one-output-one-session.test.js +0 -273
  186. package/test/one-resource-divided.test.js +0 -185
  187. package/test/one-serving-output-per-height.test.js +0 -458
  188. package/test/orchestrator-wired.test.js +0 -207
  189. package/test/output-catalog.test.js +0 -39
  190. package/test/output-shape.test.js +0 -21
  191. package/test/output-spec.test.js +0 -251
  192. package/test/output-speed.test.js +0 -92
  193. package/test/outputs-being-prepared.test.js +0 -40
  194. package/test/packet-witness-ring.test.js +0 -236
  195. package/test/packet-witness.test.js +0 -148
  196. package/test/piece-completeness.test.js +0 -78
  197. package/test/piece-disk-store.test.js +0 -401
  198. package/test/piece-from-whole-file.test.js +0 -129
  199. package/test/piece-lru.test.js +0 -319
  200. package/test/piece-reader.test.js +0 -162
  201. package/test/piece-selection-offset.test.js +0 -85
  202. package/test/piece-store-eviction.test.js +0 -595
  203. package/test/piece-store-never-refuses.test.js +0 -153
  204. package/test/piece-store-reservations.test.js +0 -403
  205. package/test/piece-store-slow-disk.test.js +0 -159
  206. package/test/piece-tail.test.js +0 -97
  207. package/test/plan-host-timings.test.js +0 -68
  208. package/test/plan-is-steady.test.js +0 -225
  209. package/test/playback-readiness.test.js +0 -518
  210. package/test/playback-start-regressions.test.js +0 -101
  211. package/test/priority-map-download.test.js +0 -174
  212. package/test/priority-map-emptied.test.js +0 -204
  213. package/test/priority-map-per-output.test.js +0 -215
  214. package/test/priority-map.test.js +0 -163
  215. package/test/probe-wedge-certainty.test.js +0 -60
  216. package/test/produced-copy-choice.test.js +0 -213
  217. package/test/published-timeline.test.js +0 -83
  218. package/test/quality-offer.test.js +0 -160
  219. package/test/quality-variants.test.js +0 -1274
  220. package/test/rate-control-in-the-output.test.js +0 -181
  221. package/test/read-survives-withdrawal.test.js +0 -164
  222. package/test/read-window.test.js +0 -365
  223. package/test/reads-not-yet-arrived.test.js +0 -218
  224. package/test/resume-warm.test.js +0 -39
  225. package/test/run-graph-drift.test.js +0 -37
  226. package/test/run-inputs.test.js +0 -80
  227. package/test/run-intervals.test.js +0 -100
  228. package/test/run-position-follows-published-grid.test.js +0 -89
  229. package/test/run-progress.test.js +0 -64
  230. package/test/same-height-switch.test.js +0 -663
  231. package/test/seek-frees-the-viewers-requests.test.js +0 -217
  232. package/test/seek-landing.test.js +0 -109
  233. package/test/seek-target-not-superseded.test.js +0 -195
  234. package/test/segment-closed-on-both-branches.test.js +0 -114
  235. package/test/segment-cut-times.test.js +0 -63
  236. package/test/segment-demand.test.js +0 -80
  237. package/test/segment-hold-supersede.test.js +0 -91
  238. package/test/segment-judged-by-its-own-header.test.js +0 -222
  239. package/test/segment-serve-wiring.test.js +0 -481
  240. package/test/segment-store-eviction.test.js +0 -247
  241. package/test/segment-store.test.js +0 -352
  242. package/test/segment-true-start.test.js +0 -86
  243. package/test/segments-are-shared.test.js +0 -176
  244. package/test/send-chunk-bytes.test.js +0 -86
  245. package/test/serving-existing-output.test.js +0 -111
  246. package/test/serving-output.test.js +0 -102
  247. package/test/shared-piece-store.test.js +0 -310
  248. package/test/sidecar-naming.test.js +0 -142
  249. package/test/soundtrack-rate-bound.test.js +0 -289
  250. package/test/source-file.test.js +0 -133
  251. package/test/source-files-route.test.js +0 -113
  252. package/test/source-registry.test.js +0 -18
  253. package/test/split-a-stretch.test.js +0 -139
  254. package/test/stale-request-after-seek.test.js +0 -158
  255. package/test/startup-readings.test.js +0 -124
  256. package/test/stream-route.test.js +0 -237
  257. package/test/subtitle-cue-framing.test.js +0 -200
  258. package/test/subtitle-cue-source.test.js +0 -102
  259. package/test/subtitle-cue-walk.test.js +0 -385
  260. package/test/subtitle-cursor.test.js +0 -63
  261. package/test/subtitle-defaults.test.js +0 -118
  262. package/test/subtitle-language.test.js +0 -252
  263. package/test/subtitle-metadata.test.js +0 -15
  264. package/test/subtitle-plan-waits.test.js +0 -125
  265. package/test/subtitle-track-numbering.test.js +0 -391
  266. package/test/supply-margin.test.js +0 -190
  267. package/test/swarm-follows-readers.test.js +0 -174
  268. package/test/swarm-reach.test.js +0 -130
  269. package/test/swarm-selection.test.js +0 -220
  270. package/test/tail-duplication.test.js +0 -167
  271. package/test/the-channel-has-both-ends.test.js +0 -91
  272. package/test/the-torrent-thread-serves-bytes.test.js +0 -107
  273. package/test/thread-pool.test.js +0 -53
  274. package/test/timeline.test.js +0 -84
  275. package/test/torrent-contents.test.js +0 -235
  276. package/test/torrent-cost.test.js +0 -71
  277. package/test/torrent-files.test.js +0 -221
  278. package/test/torrent-naming.test.js +0 -255
  279. package/test/tracks-begin-together.test.js +0 -209
  280. package/test/tunnel-renewal.test.js +0 -167
  281. package/test/two-viewers-one-picture.test.js +0 -508
  282. package/test/upload-hurry.test.js +0 -186
  283. package/test/usrsctp-state.test.js +0 -131
  284. package/test/video-facts.test.js +0 -102
  285. package/test/viewer-assignments.test.js +0 -143
  286. package/test/viewer-outputs.test.js +0 -285
  287. package/test/viewer-presence.test.js +0 -116
  288. package/test/viewer-requests-admission.test.js +0 -92
  289. package/test/viewer-starts-on-map.test.js +0 -27
  290. package/test/viewer-states.test.js +0 -192
  291. package/test/viewer-subtitle-subscription.test.js +0 -96
  292. package/test/viewer.test.js +0 -139
  293. package/test/viewing-generations.test.js +0 -390
  294. package/test/visible-rung.test.js +0 -37
  295. package/test/wait-ledger.test.js +0 -100
  296. package/test/webrtc-shim.test.js +0 -149
  297. package/test/wedge-certainty.test.js +0 -131
  298. package/test/whole-file-held-read.test.js +0 -30
  299. package/test/withdraw-piece-claim.test.js +0 -212
  300. package/test/worker-channel.test.js +0 -158
@@ -1,65 +0,0 @@
1
- /**
2
- * @file When a move to a smaller output is prepared for a viewer, and when it is
3
- * not (roadmap item 98), and which way their buffer is going.
4
- */
5
-
6
- import test from "node:test";
7
- import assert from "node:assert/strict";
8
- import { drainThreat } from "../services/encode/quality/drain-threat.js";
9
- import { bufferTrend } from "../services/viewer/buffer-trend.js";
10
-
11
- test("a buffer that falls but lasts past the new piece's readiness moves nothing", () => {
12
- // 60 s held, draining at 0.5 s/s: empty in 120 s. The new output would have
13
- // the piece in 8 s, and the next report comes in 10 s.
14
- const answer = drainThreat({ bufferedSec: 60, slope: -0.5, reportGapSec: 10, secondsToReady: 8 });
15
- assert.equal(answer.threat, false);
16
- assert.equal(answer.secondsToEmpty, 120);
17
- });
18
-
19
- test("a buffer that would end before the new piece is ready starts the move", () => {
20
- // 6 s held, draining at 0.5 s/s: empty in 12 s — sooner than 8 s of making
21
- // plus the 10 s until this viewer is heard from again.
22
- assert.equal(drainThreat({ bufferedSec: 6, slope: -0.5, reportGapSec: 10, secondsToReady: 8 }).threat, true);
23
- });
24
-
25
- test("the wait for the next report is part of the reckoning", () => {
26
- const without = drainThreat({ bufferedSec: 10, slope: -1, reportGapSec: 0, secondsToReady: 8 });
27
- const withGap = drainThreat({ bufferedSec: 10, slope: -1, reportGapSec: 3, secondsToReady: 8 });
28
- assert.equal(without.threat, false, "10 s left against 8 s of making");
29
- assert.equal(withGap.threat, true, "but the next chance to act is 3 s away, too late");
30
- });
31
-
32
- test("a buffer that holds or grows is no threat, and neither is one with no trend yet", () => {
33
- assert.equal(drainThreat({ bufferedSec: 1, slope: 0, reportGapSec: 10, secondsToReady: 30 }).threat, false);
34
- assert.equal(drainThreat({ bufferedSec: 1, slope: 0.4, reportGapSec: 10, secondsToReady: 30 }).threat, false);
35
- assert.equal(drainThreat({ bufferedSec: 1, slope: null, reportGapSec: 10, secondsToReady: 30 }).threat, false);
36
- });
37
-
38
- test("a readiness nobody can state is not assumed to be quick", () => {
39
- assert.equal(drainThreat({ bufferedSec: 600, slope: -0.1, reportGapSec: 10, secondsToReady: null }).threat, true);
40
- });
41
-
42
- test("the trend is read over one segment's period, not from two readings", () => {
43
- // A sawtooth over a flat line: a 4 s segment arrives, then 4 s play out.
44
- const at = (seconds) => seconds * 1000;
45
- const flat = [
46
- { at: at(0), seconds: 20 },
47
- { at: at(2), seconds: 18 },
48
- { at: at(4), seconds: 20 },
49
- { at: at(6), seconds: 18 },
50
- { at: at(8), seconds: 20 }
51
- ];
52
- const trend = bufferTrend(flat, 4);
53
- assert.ok(Math.abs(trend.slope) < 0.5, `the last two readings alone say +1 s/s; the period says ${trend.slope}`);
54
- assert.equal(trend.reportGapSec, 2);
55
- });
56
-
57
- test("readings that do not yet span one segment say nothing", () => {
58
- assert.equal(bufferTrend([{ at: 0, seconds: 10 }, { at: 1000, seconds: 9 }], 4), null);
59
- assert.equal(bufferTrend([{ at: 0, seconds: 10 }], 4), null);
60
- });
61
-
62
- test("a steady drain reads as the rate it drains at", () => {
63
- const readings = [0, 5, 10].map((second) => ({ at: second * 1000, seconds: 30 - second }));
64
- assert.equal(bufferTrend(readings, 4).slope, -1);
65
- });
@@ -1,48 +0,0 @@
1
- /**
2
- * @file One infohash, two sources: what happens when the second add is refused.
3
- *
4
- * The same film arrives as a magnet and as a `.torrent`. WebTorrent refuses the
5
- * second add — one infohash, one torrent — and the pool then has to decide what
6
- * to hand back. Adopting the one already there is right when it is ready, and
7
- * wrong when it is not: a magnet whose swarm has not answered has no file list,
8
- * so everything bound to it is answered 404.
9
- *
10
- * Measured 2026-08-15 on the addon host: a magnet with no reachable trackers
11
- * was added first and never became ready; the same film's own `.torrent`, which
12
- * carries the file list, the piece hashes and the trackers, then joined that
13
- * empty torrent instead of replacing it. `/stream` answered 404, the encoder
14
- * died on its first read, and the film stayed unplayable until the proxy was
15
- * restarted — one bad magnet poisoning every later attempt at that infohash.
16
- */
17
-
18
- import test from "node:test";
19
- import assert from "node:assert/strict";
20
- import { duplicateAddDecision } from "../services/torrent/torrent-pool.js";
21
-
22
- test("a ready torrent is adopted, whatever the new source carries", () => {
23
- assert.equal(
24
- duplicateAddDecision({ existingIsReady: true, incomingHasMetadata: true }),
25
- "adopt",
26
- "one swarm per infohash: a second copy would download the same pieces twice"
27
- );
28
- assert.equal(
29
- duplicateAddDecision({ existingIsReady: true, incomingHasMetadata: false }),
30
- "adopt"
31
- );
32
- });
33
-
34
- test("an unready torrent is replaced when this add brings the metadata", () => {
35
- assert.equal(
36
- duplicateAddDecision({ existingIsReady: false, incomingHasMetadata: true }),
37
- "replace",
38
- "the .torrent holds what the swarm was being asked for; waiting for it is waiting for nothing"
39
- );
40
- });
41
-
42
- test("two magnets wait, because neither has anything the other lacks", () => {
43
- assert.equal(
44
- duplicateAddDecision({ existingIsReady: false, incomingHasMetadata: false }),
45
- "wait",
46
- "replacing here would only restart the same search, and the caller's own bound answers"
47
- );
48
- });
@@ -1,170 +0,0 @@
1
- /**
2
- * @file Whether the whole machine has a place for one more encoder (roadmap
3
- * item 97, step 13).
4
- *
5
- * What is pinned here:
6
- *
7
- * 1. a place is held by an OUTPUT: two preparations onto one output take one
8
- * place, and the second is admitted without arithmetic;
9
- * 2. two preparations onto two DIFFERENT outputs with room for one: the first
10
- * is admitted, the second refused, because it is asked after the first is
11
- * recorded;
12
- * 3. one of two viewers giving up a preparation of one output does not free
13
- * its place;
14
- * 4. the cost is what an encoder costs, not a count of processes: a picture
15
- * and its soundtrack fit where two pictures do not — the addon host's own
16
- * figures, 1080p alone at 1.96x;
17
- * 5. an admitted encoder is never taken away: the machine's answer is never
18
- * below what already runs, nor below one for an output a preparation holds;
19
- * 6. a new output on a full machine is given no encoder.
20
- *
21
- * The records of who is being prepared are stood in for by a set the check
22
- * writes to, which is what the viewers' records are read as.
23
- */
24
-
25
- import test from "node:test";
26
- import assert from "node:assert/strict";
27
- import { EncodeAdmission } from "../services/encode/EncodeAdmission.js";
28
-
29
- // 1080p re-encoded on the addon host: 1.96x alone.
30
- const PICTURE_1080 = 1 / 1.96;
31
- // A re-encoded soundtrack: a small fraction of a picture.
32
- const SOUNDTRACK = 0.02;
33
-
34
- /**
35
- * A machine with the outputs named, their costs, what runs and who is being
36
- * prepared onto what.
37
- *
38
- * @param {{ costs: Record<string, number | null>, running?: Record<string, number>, share?: number | null }} how
39
- */
40
- function machine(how) {
41
- const running = new Map(Object.entries(how.running ?? {}));
42
- /** Output address to the viewers being prepared onto it. @type {Map<string, Set<string>>} */
43
- const preparing = new Map();
44
- const admission = new EncodeAdmission({
45
- liveRunsByAddress: () => new Map([...running].filter(([, count]) => count > 0)),
46
- preparedAddresses: () => new Set([...preparing].filter(([, viewers]) => viewers.size > 0).map(([address]) => address)),
47
- loadOf: (address) => (address in how.costs ? { costSec: how.costs[address], fileKey: "", fileCostSec: 0 } : null),
48
- availability: () => (how.share === undefined || how.share === null ? null : { share: how.share, known: true })
49
- });
50
- return {
51
- admission,
52
- running,
53
- /** A viewer's preparation recorded, as `Viewer` records it. */
54
- prepare: (address, viewer) => {
55
- const viewers = preparing.get(address) ?? new Set();
56
- viewers.add(viewer);
57
- preparing.set(address, viewers);
58
- },
59
- /** A viewer's preparation ended, on whichever path. */
60
- giveUp: (address, viewer) => preparing.get(address)?.delete(viewer)
61
- };
62
- }
63
-
64
- test("two preparations onto one output take one place", () => {
65
- const { admission, prepare } = machine({ costs: { a: PICTURE_1080, b: PICTURE_1080 } });
66
- assert.equal(admission.admitsPreparation("a").admitted, true);
67
- prepare("a", "viewer-1");
68
- const second = admission.admitsPreparation("a");
69
- assert.equal(second.admitted, true, "the second viewer shares the place");
70
- assert.match(second.reason, /already holds a place/);
71
- });
72
-
73
- test("two preparations onto two different outputs with room for one: the second is refused", () => {
74
- const { admission, prepare } = machine({ costs: { a: PICTURE_1080, b: PICTURE_1080 } });
75
- // The first is asked and recorded in one stretch, as the preparation does it.
76
- assert.equal(admission.admitsPreparation("a").admitted, true);
77
- prepare("a", "viewer-1");
78
- const second = admission.admitsPreparation("b");
79
- assert.equal(second.admitted, false, "both together would make 0.98x of each");
80
- assert.ok(second.speedX < 1);
81
- });
82
-
83
- test("one of two viewers giving up a preparation of one output does not free its place", () => {
84
- const { admission, prepare, giveUp } = machine({ costs: { a: PICTURE_1080, b: PICTURE_1080 } });
85
- prepare("a", "viewer-1");
86
- prepare("a", "viewer-2");
87
- giveUp("a", "viewer-1");
88
- assert.equal(admission.admitsPreparation("b").admitted, false, "viewer-2 still holds the place on a");
89
- assert.equal(admission.placesFor("a", 0).runs, 1, "and a keeps its encoder's place");
90
- giveUp("a", "viewer-2");
91
- assert.equal(admission.admitsPreparation("b").admitted, true, "the place ends with the last record");
92
- });
93
-
94
- test("a picture and its soundtrack fit where two pictures do not", () => {
95
- const { admission } = machine({
96
- costs: { picture: PICTURE_1080, sound: SOUNDTRACK, other: PICTURE_1080 },
97
- running: { picture: 1 }
98
- });
99
- assert.equal(admission.admitsPreparation("sound").admitted, true, "0.53 s/s makes 1.89x");
100
- assert.equal(admission.placesFor("sound", 0).runs >= 1, true);
101
- assert.equal(admission.admitsPreparation("other").admitted, false, "1.02 s/s makes 0.98x");
102
- });
103
-
104
- test("an admitted encoder is never taken away, and a held place is kept", () => {
105
- const { admission } = machine({
106
- costs: { a: PICTURE_1080, b: PICTURE_1080 },
107
- running: { a: 1, b: 1 }
108
- });
109
- assert.equal(admission.placesFor("a", 1).runs, 1, "both running: each keeps its one");
110
- assert.equal(admission.placesFor("b", 1).runs, 1);
111
- const { admission: held, prepare: prepareHeld } = machine({
112
- costs: { a: PICTURE_1080, b: PICTURE_1080 },
113
- running: { b: 1 }
114
- });
115
- prepareHeld("a", "viewer-1");
116
- assert.equal(held.placesFor("a", 0).runs, 1, "a place promised to a preparation is not withdrawn");
117
- });
118
-
119
- test("a new output on a full machine is given no encoder", () => {
120
- const { admission } = machine({ costs: { a: PICTURE_1080, b: PICTURE_1080 }, running: { a: 1 } });
121
- assert.equal(admission.placesFor("b", 0).runs, 0);
122
- });
123
-
124
- test("the share of the machine nobody has priced is taken off what there is", () => {
125
- // 0.6 s/s alone makes 1.67x; with a third of the machine taken by work
126
- // nobody priced it makes 1.11x, and a soundtrack beside it still fits.
127
- const quiet = machine({ costs: { a: 0.6, b: 0.4 }, share: 1 });
128
- assert.equal(quiet.admission.admitsPreparation("a").admitted, true);
129
- const busy = machine({ costs: { a: 0.6, b: 0.4 }, running: { a: 1 }, share: 0.6 });
130
- assert.equal(busy.admission.admitsPreparation("b").admitted, false, "1.0 s/s at 0.6 of the machine makes 0.6x");
131
- });
132
-
133
- test("an output with no measured cost is not admitted", () => {
134
- const { admission } = machine({ costs: { a: PICTURE_1080, b: null }, running: { a: 1 } });
135
- const answer = admission.admitsPreparation("b");
136
- assert.equal(answer.admitted, false);
137
- assert.match(answer.reason, /no measured encoding cost/);
138
- });
139
-
140
- test("unpriced occupied work is neither spare pool headroom nor capacity for another output", () => {
141
- const { admission } = machine({ costs: { a: null, b: PICTURE_1080 }, running: { a: 1 } });
142
- assert.equal(admission.headroom().encodeSpeedX, 0, "the pool sees no confirmed room");
143
- const answer = admission.admitsPreparation("b");
144
- assert.equal(answer.admitted, false);
145
- assert.match(answer.reason, /capacity is unknown/);
146
- });
147
-
148
- test("a near-free output is answered at once, whatever its price", () => {
149
- // Field 2026-10-04: a soundtrack priced at 5.4e-14 s per film second kept
150
- // the main thread counting encoders one at a time for minutes.
151
- const { admission } = machine({ costs: { sound: 5.36680075607109e-14 }, share: 0.6 });
152
- const started = Date.now();
153
- const { runs } = admission.placesFor("sound", 0);
154
- assert.ok(Date.now() - started < 1_000, "answered without counting one at a time");
155
- assert.ok(runs > 1e12 && Number.isFinite(runs), `as many as fit: ${runs}`);
156
- });
157
-
158
- test("the count is the largest that keeps every output at realtime", () => {
159
- for (const cost of [0.5, 0.1, 0.03, 0.007, 0.0011]) {
160
- for (const share of [null, 1, 0.37]) {
161
- const { admission } = machine({ costs: { a: cost, b: PICTURE_1080 / 4 }, running: { b: 1 }, share });
162
- const usable = share ?? 1;
163
- let expected = 0;
164
- while (usable / (PICTURE_1080 / 4 + (expected + 1) * cost) >= 1) {
165
- expected += 1;
166
- }
167
- assert.equal(admission.placesFor("a", 0).runs, Math.max(expected, 0), `cost ${cost}, share ${share}`);
168
- }
169
- }
170
- });
@@ -1,275 +0,0 @@
1
- /**
2
- * @file What encoding costs this machine, asked of the object that owns it.
3
- *
4
- * The arithmetic itself is exercised end to end by `auto-quality-step` and
5
- * `quality-variants`, which go through the session manager. What is pinned here
6
- * is the seam the move created: this object is given the host's readings as a
7
- * QUESTION rather than a copy, and it holds what an encoder taught it.
8
- */
9
-
10
- import test from "node:test";
11
- import assert from "node:assert/strict";
12
- import { EncodeCost } from "../services/encode/quality/EncodeCost.js";
13
- import { measureSpeed, startRunOn } from "./helpers/encode-run.js";
14
- import { outputSpec } from "./helpers/output-spec.js";
15
- import { runStateOf } from "../services/encode/encode-run-state.js";
16
- import { qualityStateOf } from "../services/encode/quality/OutputQualityState.js";
17
-
18
- /**
19
- * @param {object} [readings]
20
- * @returns {{ cost: EncodeCost, asked: () => number, host: { share: number } }}
21
- */
22
- function costOn(readings = {}) {
23
- let asked = 0;
24
- const host = { share: 1 };
25
- const cost = new EncodeCost({
26
- outputs: {
27
- familyOf: (session) => readings.familyOf?.(session) ?? [],
28
- variantHeightOf: () => 0
29
- },
30
- host: () => {
31
- asked += 1;
32
- return {
33
- benchmark: readings.benchmark ?? null,
34
- decodeModel: null,
35
- contentionPenalties: null,
36
- availability: { known: true, share: host.share }
37
- };
38
- },
39
- runningEncoders: () => readings.runningEncoders?.() ?? 0,
40
- encodersRunningNow: () => 0,
41
- torrentCostSecFor: () => 0,
42
- runsFor: (session) => [...(session.runs ?? [])],
43
- stateFor: (session) => runStateOf(session.runs)
44
- });
45
- return { cost, asked: () => asked, host };
46
- }
47
-
48
- /**
49
- * A session with one run going, of the kind this file measures.
50
- *
51
- * @param {{ audioOnly?: boolean, transcodeVideo?: boolean, audioSourceTrackIndex?: number, key?: string }} [what]
52
- * @returns {object}
53
- */
54
- function sessionProducing(what = {}) {
55
- const session = {
56
- id: "1111111122223333",
57
- state: "ready",
58
- spec: outputSpec({
59
- audioOnly: what.audioOnly === true,
60
- transcodeVideo: what.transcodeVideo === true,
61
- audioSourceTrackIndex: what.audioSourceTrackIndex ?? 0
62
- }),
63
- file: { key: what.key ?? "torrent:abc:0", name: "film.mkv" },
64
- output: { encodeWidth: 0, encodeHeight: 0, outputFps: 25, softwarePreset: null },
65
- clock: { at: 1000 },
66
- runs: new Set()
67
- };
68
- startRunOn(session, { from: 0, clock: session.clock });
69
- return session;
70
- }
71
-
72
- /**
73
- * The session's run measures its speed, and is learned from as the product
74
- * learns: once the run has a reading.
75
- *
76
- * @param {EncodeCost} cost
77
- * @param {object} session
78
- * @param {number} speedX - Seconds of film produced per second of the run's own work.
79
- * @param {number} [waitedMs] - How long the run's input waits for the swarm
80
- * between two closed pieces.
81
- * @returns {Promise<void>}
82
- */
83
- async function watchItRun(cost, session, speedX, waitedMs = 0) {
84
- const run = [...session.runs][0];
85
- measureSpeed(run, speedX, session.clock, waitedMs);
86
- await cost.learnFrom(session, run);
87
- }
88
-
89
- test("the host is asked at the moment of the question, not when this was built", () => {
90
- // The share of the machine that is free is re-read every few seconds. Copied
91
- // into this object when it was made, every later rung would be priced against
92
- // a machine that has gone.
93
- const { cost, asked, host } = costOn();
94
- assert.equal(asked(), 0, "nothing is read until something is asked");
95
-
96
- cost.sustainableHeights({ heights: [1080], sourceWidth: 1920, sourceHeight: 1080, fps: 24, source: null, transcodeVideo: true, ownHeight: 0 });
97
- const first = asked();
98
- assert.ok(first > 0);
99
-
100
- host.share = 0.1;
101
- cost.sustainableHeights({ heights: [1080], sourceWidth: 1920, sourceHeight: 1080, fps: 24, source: null, transcodeVideo: true, ownHeight: 0 });
102
- assert.ok(asked() > first, "and read again on the next question");
103
- });
104
-
105
- test("with no benchmark to judge by, every height offered is kept", () => {
106
- // Nothing measured is not the same as nothing possible. Refusing here would
107
- // hide the whole ladder on a host whose startup measurement failed.
108
- const { cost } = costOn({ benchmark: null });
109
- const kept = cost.sustainableHeights({
110
- heights: [1080, 720, 480],
111
- sourceWidth: 1920,
112
- sourceHeight: 1080,
113
- fps: 24,
114
- source: null,
115
- transcodeVideo: true,
116
- ownHeight: 0
117
- });
118
- assert.deepEqual(kept, [1080, 720, 480]);
119
- });
120
-
121
- test("a rung measured below realtime is withdrawn, and a copied source height is not", () => {
122
- const { cost } = costOn({ benchmark: null });
123
- const kept = cost.sustainableHeights({
124
- heights: [1080, 480],
125
- sourceWidth: 1920,
126
- sourceHeight: 1080,
127
- fps: 24,
128
- source: null,
129
- // The base copies its picture, so the source height costs no encoder.
130
- transcodeVideo: false,
131
- ownHeight: 1080,
132
- measuredHeights: new Map([[480, 0.4]])
133
- });
134
- assert.deepEqual(kept, [1080], "the copy stays; the rung seen failing does not");
135
- });
136
-
137
- test("what an encoder taught this host is learned here, and held here", () => {
138
- // The three that learn were methods of the session manager, writing into maps
139
- // this object owned — one keeper, a different author, and the maps public for
140
- // it. Nothing outside can write a price now, and nothing outside needs to.
141
- const { cost } = costOn();
142
-
143
- assert.equal(typeof cost.learnFrom, "function");
144
- assert.equal(cost.copyCost, undefined, "the store is not the surface");
145
- assert.equal(cost.decodeCostFor("torrent:abc:0"), null, "and nothing is known until an encoder says so");
146
- });
147
-
148
- test("a copy is priced from what it was seen doing", async () => {
149
- const { cost } = costOn();
150
- const session = sessionProducing({ transcodeVideo: false });
151
-
152
- await watchItRun(cost, session, 8);
153
-
154
- assert.equal(cost.copyVersionFor("torrent:abc:0"), 1, "the price is published, so every offer recomputes");
155
- assert.equal(
156
- qualityStateOf(session).lastAloneSpeed.toFixed(2),
157
- "8.00",
158
- "and what it did with the machine to itself is on the session, which withdraws a step seen failing"
159
- );
160
- });
161
-
162
- test("the time a run's input waited for the swarm is not in the price of this host", async () => {
163
- // Field 2026-10-01: a copy waited 32.93 s and 43.91 s for two pieces and read
164
- // 0.21x over the clock. Over its own working time it is the machine's speed.
165
- const { cost } = costOn();
166
- const session = sessionProducing({ transcodeVideo: false });
167
-
168
- await watchItRun(cost, session, 8, 9_000);
169
-
170
- assert.equal(qualityStateOf(session).lastAloneSpeed, 8, "the speed is the run's own, not the swarm's");
171
- assert.equal(cost.copyVersionFor("torrent:abc:0"), 1, "and it is filed as what copying costs here");
172
- });
173
-
174
- test("a picture's reading taken beside another encoder is not filed", async () => {
175
- // It contains that other encoder's work, and the budget ADDS the same work
176
- // again when it predicts — so the price is counted twice and grows with every
177
- // reading. Measured in the field 2026-08-15: copying, whose truth is 7.9x, was
178
- // learned as 2.03x, and the quality menu collapsed to one height.
179
- const { cost } = costOn({ runningEncoders: () => 2 });
180
- const session = sessionProducing({ transcodeVideo: false });
181
-
182
- await watchItRun(cost, session, 8);
183
-
184
- assert.equal(cost.copyVersionFor("torrent:abc:0"), 0);
185
- });
186
-
187
- test("a run that has measured nothing teaches nothing", async () => {
188
- const { cost } = costOn();
189
- const session = sessionProducing({ transcodeVideo: false });
190
- const run = [...session.runs][0];
191
-
192
- await cost.learnFrom(session, run);
193
-
194
- assert.equal(run.speedReading, null, "a speed needs two closed pieces of one run");
195
- assert.equal(cost.copyVersionFor("torrent:abc:0"), 0);
196
- });
197
-
198
- test("a restarted run starts its own reckoning, so a seek is never read as speed", async () => {
199
- // Twenty minutes of film against five seconds of work is a seek, and filed as
200
- // a price it admits every step there is. The reading belongs to the run that
201
- // made it, so a new run at another place has none until its own pieces close.
202
- const { cost } = costOn();
203
- const session = sessionProducing({ transcodeVideo: false });
204
- session.runs = new Set();
205
- const moved = startRunOn(session, { from: 300, clock: session.clock });
206
-
207
- await cost.learnFrom(session, moved);
208
-
209
- assert.equal(cost.copyVersionFor("torrent:abc:0"), 0);
210
- });
211
-
212
- test("two soundtracks of one file are priced apart", async () => {
213
- // The key names the TRACK, not the file. A dub and the original run at
214
- // different speeds — different codecs, different channel counts — and one key
215
- // for both files each over the other's readings.
216
- const { cost } = costOn();
217
- const first = sessionProducing({ audioOnly: true });
218
- const second = sessionProducing({ audioOnly: true, audioSourceTrackIndex: 1 });
219
-
220
- await watchItRun(cost, first, 40);
221
-
222
- assert.equal(cost.audioVersionFor(first), 1);
223
- assert.equal(cost.audioVersionFor(second), 0, "the other track has been told nothing about itself");
224
- });
225
-
226
- test("a soundtrack's own share is what is left after everything else running is paid for", async () => {
227
- // A rendition runs for exactly as long as the picture it accompanies, so
228
- // "alone" is a state it is never in and the price stayed unmeasured for ever.
229
- // Its share is recovered by subtracting what the machine is already known to
230
- // be spending — which is only possible once that other work HAS a price.
231
- const picture = sessionProducing({ transcodeVideo: false });
232
- const sound = sessionProducing({ audioOnly: true });
233
- let running = 1;
234
- const { cost } = costOn({
235
- runningEncoders: () => running,
236
- familyOf: () => [picture, sound]
237
- });
238
-
239
- // The picture alone first, so copying has a price: 8x, i.e. 0.125 s/s.
240
- await watchItRun(cost, picture, 8);
241
- assert.equal(cost.copyVersionFor("torrent:abc:0"), 1);
242
-
243
- // Now the pair. The reading off the soundtrack contains both, at 4x — 0.25 s/s
244
- // together — so the sound's own share is 0.25 − 0.125 = 0.125 s/s.
245
- running = 2;
246
- await watchItRun(cost, sound, 4);
247
-
248
- assert.equal(cost.audioVersionFor(sound), 1, "the soundtrack has a price at last");
249
- // What was filed, read back the way the product reads it: the sum of what
250
- // everything OTHER than the picture is costing. Charged the whole reading
251
- // instead of its own share, a soundtrack is priced at twice the truth and
252
- // refuses quality steps the host could hold.
253
- running = 1;
254
- assert.equal(
255
- cost.pricedConcurrentCost(picture).toFixed(3),
256
- "0.125",
257
- "0.25 s/s together, less the 0.125 s/s the copy was already known to cost"
258
- );
259
- });
260
-
261
- test("nothing is attributed to a soundtrack while something running has no price", async () => {
262
- const picture = sessionProducing({ transcodeVideo: true });
263
- const sound = sessionProducing({ audioOnly: true });
264
- const { cost } = costOn({
265
- runningEncoders: () => 2,
266
- // A re-encoded picture with no benchmark to price it by and no speed of its
267
- // own: unpriced work, which would otherwise land in the soundtrack's account
268
- // and refuse quality steps on it.
269
- familyOf: () => [picture, sound]
270
- });
271
-
272
- await watchItRun(cost, sound, 4);
273
-
274
- assert.equal(cost.audioVersionFor(sound), 0);
275
- });
@@ -1,111 +0,0 @@
1
- /**
2
- * @file The four things an ffmpeg exit can mean, by their field cases.
3
- *
4
- * Each test names the release the classification cost. None of them needs a
5
- * process: what went wrong in every case was the reading of the exit, not the
6
- * handling of it.
7
- */
8
-
9
- import assert from "node:assert/strict";
10
- import test from "node:test";
11
-
12
- import { ENCODE_EXIT, classifyEncodeExit } from "../services/encode/encode-exit.js";
13
-
14
- test("the predecessor a seek kills says nothing about the session", () => {
15
- // The handler decides "is this exit mine" by comparing against the session's
16
- // current process — and during a restart that field still names the process
17
- // being killed, because the replacement is spawned afterwards. So a SIGKILLed
18
- // predecessor passed the check and was read as the session's own run dying:
19
- // a spurious "failed" that a segment request landing in that window is
20
- // answered 500 for, and on a hardware host a permanent downgrade to libx264.
21
- assert.equal(
22
- classifyEncodeExit({ superseded: true, code: null, inputUnavailable: false }),
23
- ENCODE_EXIT.IGNORED
24
- );
25
- // Including when it looks like a clean finish.
26
- assert.equal(classifyEncodeExit({ superseded: true, code: 0 }), ENCODE_EXIT.IGNORED);
27
- });
28
-
29
- test("exit 0 short of the last segment is a failure, not a finished file", () => {
30
- // 2.9.104. A run that had made 188 segments of 624 reported itself complete,
31
- // the player consumed what was on disk and froze on the first segment nobody
32
- // was making.
33
- assert.equal(
34
- classifyEncodeExit({ code: 0, producedThrough: 188, lastSegmentIndex: 623 }),
35
- ENCODE_EXIT.SHORT
36
- );
37
- assert.equal(
38
- classifyEncodeExit({ code: 0, producedThrough: 623, lastSegmentIndex: 623 }),
39
- ENCODE_EXIT.COMPLETE
40
- );
41
- });
42
-
43
- test("exit 0 is complete when there is nothing to check it against", () => {
44
- // No published last segment, or nothing readable on disk: the claim cannot be
45
- // contradicted, so it stands. Guessing "short" here would fail every session
46
- // whose directory could not be read.
47
- assert.equal(classifyEncodeExit({ code: 0, producedThrough: null, lastSegmentIndex: 623 }), ENCODE_EXIT.COMPLETE);
48
- assert.equal(classifyEncodeExit({ code: 0, producedThrough: 12, lastSegmentIndex: null }), ENCODE_EXIT.COMPLETE);
49
- });
50
-
51
- test("data that went away is recoverable, and is not an encoder fault", () => {
52
- // Field 2026-08-06: a torrent evicted mid-seek took the film with it, the run
53
- // died on `File 0 not found`, and the session answered 500 from then on while
54
- // the swarm was there and the data would have come back in seconds.
55
- assert.equal(
56
- classifyEncodeExit({ code: 1, inputUnavailable: true }),
57
- ENCODE_EXIT.INPUT_LOST
58
- );
59
- // And the distinction is what keeps a working hardware encoder: the caller
60
- // downgrades to software on FAILED, so an input that vanished must not
61
- // arrive there.
62
- assert.notEqual(classifyEncodeExit({ code: 1, inputUnavailable: true }), ENCODE_EXIT.FAILED);
63
- });
64
-
65
- test("anything else is a failure of this target", () => {
66
- assert.equal(classifyEncodeExit({ code: 1, inputUnavailable: false }), ENCODE_EXIT.FAILED);
67
- assert.equal(classifyEncodeExit({ code: null, inputUnavailable: false }), ENCODE_EXIT.FAILED);
68
- });
69
-
70
- test("every combination answers, and no input produces undefined", () => {
71
- for (const superseded of [true, false]) {
72
- for (const code of [0, 1, null]) {
73
- for (const producedThrough of [null, 0, 10]) {
74
- for (const lastSegmentIndex of [null, 0, 10]) {
75
- for (const inputUnavailable of [true, false]) {
76
- const answer = classifyEncodeExit({
77
- superseded,
78
- code,
79
- producedThrough,
80
- lastSegmentIndex,
81
- inputUnavailable
82
- });
83
- assert.ok(
84
- Object.values(ENCODE_EXIT).includes(answer),
85
- `answered ${String(answer)} for ${JSON.stringify({ superseded, code, producedThrough, lastSegmentIndex, inputUnavailable })}`
86
- );
87
- }
88
- }
89
- }
90
- }
91
- }
92
- assert.ok(Object.values(ENCODE_EXIT).includes(classifyEncodeExit()));
93
- });
94
-
95
- test("a run that made nothing has not finished, whatever its exit code", () => {
96
- // Field 2026-09-11: a run given #541..#541 was handed a start later than its
97
- // own end, wrote 190 bytes that are not a fragment, and exited zero. Making
98
- // no segment and being unable to read the directory were the same `null`, and
99
- // the second reading — "cannot be contradicted, so it stands" — was applied
100
- // to the first.
101
- assert.equal(
102
- classifyEncodeExit({ code: 0, producedThrough: null, producedCount: 0, lastSegmentIndex: 541 }),
103
- ENCODE_EXIT.SHORT
104
- );
105
- // And the case that reading was written for still stands: nothing readable on
106
- // disk is not a claim that nothing was made.
107
- assert.equal(
108
- classifyEncodeExit({ code: 0, producedThrough: null, producedCount: null, lastSegmentIndex: 623 }),
109
- ENCODE_EXIT.COMPLETE
110
- );
111
- });