@torrent-tv/proxy 2.73.0 → 2.74.0

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 (35) hide show
  1. package/CHANGELOG.md +1447 -1432
  2. package/CLAUDE.md +165 -160
  3. package/docs/container-architecture.md +192 -184
  4. package/package.json +1 -1
  5. package/routes/api/subtitles/get.js +205 -205
  6. package/services/container/Container.js +354 -135
  7. package/services/container/MatroskaContainer.js +1155 -516
  8. package/services/container/Mp4Container.js +858 -392
  9. package/services/container/SubtitleFileContainer.js +323 -261
  10. package/services/controllers/SubtitleController.js +128 -127
  11. package/services/delivery-probe.js +64 -6
  12. package/services/hls-session-manager.js +32 -35
  13. package/services/language-detect.js +174 -228
  14. package/services/orchestrators/SubtitleOrchestrator.js +64 -5
  15. package/services/playback-planner.js +747 -747
  16. package/services/produced-index.js +300 -0
  17. package/services/torrent-worker/subtitle-cues.js +582 -618
  18. package/services/tracks/TextSubtitleTrack.js +287 -47
  19. package/services/tracks/index.js +14 -14
  20. package/test/delivery-probe.test.js +67 -0
  21. package/test/matroska-blocks.test.js +0 -0
  22. package/test/mp4-subtitles.test.js +173 -127
  23. package/test/produced-index.test.js +188 -0
  24. package/test/subtitle-cue-framing.test.js +200 -202
  25. package/test/subtitle-cue-source.test.js +104 -0
  26. package/test/subtitle-cue-walk.test.js +369 -0
  27. package/test/subtitle-defaults.test.js +97 -97
  28. package/test/subtitle-language.test.js +252 -252
  29. package/test/subtitle-track-numbering.test.js +370 -370
  30. package/services/container-index/matroska-blocks.js +0 -202
  31. package/services/container-index/matroska-subtitles.js +0 -372
  32. package/services/container-index/mp4-subtitles.js +0 -404
  33. package/services/subtitle-convert.js +0 -144
  34. package/services/subtitle-defaults.js +0 -157
  35. package/services/tracks/subtitle-markup.js +0 -104
@@ -1,127 +1,173 @@
1
- /**
2
- * @file Reading an MP4's text subtitle track out of its sample table.
3
- *
4
- * The file is built here, so every answer is known in advance: two cues at
5
- * stated times, in stated places, with an empty sample between them — the way
6
- * the format says "nothing on screen just now".
7
- */
8
-
9
- import test from "node:test";
10
- import assert from "node:assert/strict";
11
- import { decodeSubtitleSample, readMp4SubtitlePlan } from "../services/container-index/mp4-subtitles.js";
12
-
13
- function box(type, payload) {
14
- const header = Buffer.alloc(8);
15
- header.writeUInt32BE(payload.length + 8, 0);
16
- header.write(type, 4, "latin1");
17
- return Buffer.concat([header, payload]);
18
- }
19
-
20
- function fullBox(type, payload) {
21
- return box(type, Buffer.concat([Buffer.from([0, 0, 0, 0]), payload]));
22
- }
23
-
24
- function u32(...values) {
25
- const buffer = Buffer.alloc(values.length * 4);
26
- values.forEach((value, index) => buffer.writeUInt32BE(value, index * 4));
27
- return buffer;
28
- }
29
-
30
- /** A `tx3g` sample: a two-byte length, then the text. */
31
- function textSample(text) {
32
- const bytes = Buffer.from(text, "utf8");
33
- const length = Buffer.alloc(2);
34
- length.writeUInt16BE(bytes.length, 0);
35
- return Buffer.concat([length, bytes]);
36
- }
37
-
38
- /**
39
- * A file with one text track: three samples, the middle one empty.
40
- *
41
- * @returns {{ file: Buffer, samples: Buffer[] }}
42
- */
43
- function buildFile() {
44
- const samples = [textSample("First line"), Buffer.alloc(2), textSample("Second line")];
45
- const timescale = 1000;
46
-
47
- const mdhd = fullBox("mdhd", Buffer.concat([
48
- u32(0, 0, timescale, 60_000),
49
- // Language "eng", five bits a letter offset from 0x60, then a spare word.
50
- Buffer.from([0x15, 0xc7, 0, 0])
51
- ]));
52
- const hdlr = fullBox("hdlr", Buffer.concat([u32(0), Buffer.from("sbtl", "latin1"), u32(0, 0, 0)]));
53
- const tkhd = fullBox("tkhd", u32(0, 0, 7, 0, 60_000));
54
-
55
- const stsd = fullBox("stsd", Buffer.concat([u32(1), box("tx3g", Buffer.alloc(24))]));
56
- // Two seconds a sample, so the cues sit at 0-2, 2-4 and 4-6 seconds.
57
- const stts = fullBox("stts", Buffer.concat([u32(1), u32(3, 2000)]));
58
- const stsz = fullBox("stsz", Buffer.concat([u32(0, 3), u32(...samples.map((s) => s.length))]));
59
- const stsc = fullBox("stsc", Buffer.concat([u32(1), u32(1, 3, 1)]));
60
-
61
- const stbl = box("stbl", Buffer.concat([stsd, stts, stsz, stsc, fullBox("stco", Buffer.concat([u32(1), u32(0)]))]));
62
- const minf = box("minf", stbl);
63
- const mdia = box("mdia", Buffer.concat([mdhd, hdlr, minf]));
64
- const trak = box("trak", Buffer.concat([tkhd, mdia]));
65
- const moovDraft = box("moov", trak);
66
-
67
- // The samples sit after moov, so the chunk offset is known only now. Its
68
- // length does not change when the placeholder becomes the real value.
69
- const mdatStart = moovDraft.length + 8;
70
- const stcoReal = fullBox("stco", Buffer.concat([u32(1), u32(mdatStart)]));
71
- const stblReal = box("stbl", Buffer.concat([stsd, stts, stsz, stsc, stcoReal]));
72
- const moov = box("moov", box("trak", Buffer.concat([
73
- tkhd,
74
- box("mdia", Buffer.concat([mdhd, hdlr, box("minf", stblReal)]))
75
- ])));
76
- const mdat = box("mdat", Buffer.concat(samples));
77
- return { file: Buffer.concat([moov, mdat]), samples };
78
- }
79
-
80
- function readerOver(file) {
81
- return async (start, end) => {
82
- const last = Math.min(end, file.length - 1);
83
- return start > last ? null : file.subarray(start, last + 1);
84
- };
85
- }
86
-
87
- test("a text track's cues are found with their times and their byte ranges", async () => {
88
- const { file, samples } = buildFile();
89
-
90
- const plan = await readMp4SubtitlePlan(readerOver(file), file.length);
91
-
92
- assert.equal(plan.tracks.length, 1);
93
- const track = plan.tracks[0];
94
- assert.equal(track.format, "tx3g");
95
- assert.equal(track.language, "eng");
96
- assert.equal(track.samples.length, 2, "the empty sample is a gap, not a cue");
97
- assert.deepEqual(
98
- track.samples.map((sample) => [sample.startSeconds, sample.endSeconds]),
99
- [[0, 2], [4, 6]],
100
- "times come from the duration table, and the gap keeps its place in it"
101
- );
102
- // The bytes the plan points at are the ones that hold the text.
103
- const first = file.subarray(track.samples[0].offset, track.samples[0].offset + track.samples[0].size);
104
- assert.equal(decodeSubtitleSample(first, "tx3g"), "First line");
105
- const second = file.subarray(track.samples[1].offset, track.samples[1].offset + track.samples[1].size);
106
- assert.equal(decodeSubtitleSample(second, "tx3g"), "Second line");
107
- assert.equal(second.length, samples[2].length);
108
- });
109
-
110
- test("a file with no text track offers nothing", async () => {
111
- const moov = box("moov", box("trak", box("mdia", Buffer.concat([
112
- fullBox("mdhd", Buffer.concat([u32(0, 0, 1000, 100), Buffer.from([0, 0, 0, 0])])),
113
- fullBox("hdlr", Buffer.concat([u32(0), Buffer.from("vide", "latin1"), u32(0, 0, 0)]))
114
- ]))));
115
- const file = Buffer.concat([moov, box("mdat", Buffer.alloc(4))]);
116
-
117
- const plan = await readMp4SubtitlePlan(readerOver(file), file.length);
118
-
119
- assert.deepEqual(plan.tracks, []);
120
- });
121
-
122
- test("a WebVTT sample gives up the text inside its payload box", () => {
123
- const payl = box("payl", Buffer.from("Hello there", "utf8"));
124
- const sample = box("vttc", payl);
125
-
126
- assert.equal(decodeSubtitleSample(sample, "wvtt"), "Hello there");
127
- });
1
+ /**
2
+ * @file Reading an MP4's text subtitle track out of its sample table.
3
+ *
4
+ * The file is built here, so every answer is known in advance: two cues at
5
+ * stated times, in stated places, with an empty sample between them — the way
6
+ * the format says "nothing on screen just now".
7
+ */
8
+
9
+ import test from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import { Mp4Container } from "../services/container/Mp4Container.js";
12
+
13
+ function box(type, payload) {
14
+ const header = Buffer.alloc(8);
15
+ header.writeUInt32BE(payload.length + 8, 0);
16
+ header.write(type, 4, "latin1");
17
+ return Buffer.concat([header, payload]);
18
+ }
19
+
20
+ function fullBox(type, payload) {
21
+ return box(type, Buffer.concat([Buffer.from([0, 0, 0, 0]), payload]));
22
+ }
23
+
24
+ function u32(...values) {
25
+ const buffer = Buffer.alloc(values.length * 4);
26
+ values.forEach((value, index) => buffer.writeUInt32BE(value, index * 4));
27
+ return buffer;
28
+ }
29
+
30
+ /** A `tx3g` sample: a two-byte length, then the text. */
31
+ function textSample(text) {
32
+ const bytes = Buffer.from(text, "utf8");
33
+ const length = Buffer.alloc(2);
34
+ length.writeUInt16BE(bytes.length, 0);
35
+ return Buffer.concat([length, bytes]);
36
+ }
37
+
38
+ /**
39
+ * A file with one text track: three samples, the middle one empty.
40
+ *
41
+ * @param {number} [displayFlags] - The `tx3g` sample entry's display flags.
42
+ * @param {boolean} [withFtyp] - Open the file with an `ftyp` box, which is what
43
+ * the container layer sniffs for before it will read anything.
44
+ * @returns {{ file: Buffer, samples: Buffer[] }}
45
+ */
46
+ function buildFile(displayFlags = 0, withFtyp = false) {
47
+ const samples = [textSample("First line"), Buffer.alloc(2), textSample("Second line")];
48
+ const timescale = 1000;
49
+
50
+ const mdhd = fullBox("mdhd", Buffer.concat([
51
+ u32(0, 0, timescale, 60_000),
52
+ // Language "eng", five bits a letter offset from 0x60, then a spare word.
53
+ Buffer.from([0x15, 0xc7, 0, 0])
54
+ ]));
55
+ const hdlr = fullBox("hdlr", Buffer.concat([u32(0), Buffer.from("sbtl", "latin1"), u32(0, 0, 0)]));
56
+ const tkhd = fullBox("tkhd", u32(0, 0, 7, 0, 60_000));
57
+
58
+ // A sample entry opens with 6 reserved bytes and a 2-byte data reference
59
+ // index, and `displayFlags` is the 32 bits after them.
60
+ const tx3g = Buffer.alloc(24);
61
+ tx3g.writeUInt32BE(displayFlags >>> 0, 8);
62
+ const stsd = fullBox("stsd", Buffer.concat([u32(1), box("tx3g", tx3g)]));
63
+ // Two seconds a sample, so the cues sit at 0-2, 2-4 and 4-6 seconds.
64
+ const stts = fullBox("stts", Buffer.concat([u32(1), u32(3, 2000)]));
65
+ const stsz = fullBox("stsz", Buffer.concat([u32(0, 3), u32(...samples.map((s) => s.length))]));
66
+ const stsc = fullBox("stsc", Buffer.concat([u32(1), u32(1, 3, 1)]));
67
+
68
+ const stbl = box("stbl", Buffer.concat([stsd, stts, stsz, stsc, fullBox("stco", Buffer.concat([u32(1), u32(0)]))]));
69
+ const minf = box("minf", stbl);
70
+ const mdia = box("mdia", Buffer.concat([mdhd, hdlr, minf]));
71
+ const trak = box("trak", Buffer.concat([tkhd, mdia]));
72
+ const moovDraft = box("moov", trak);
73
+
74
+ // The samples sit after moov, so the chunk offset is known only now. Its
75
+ // length does not change when the placeholder becomes the real value.
76
+ const ftyp = withFtyp
77
+ ? box("ftyp", Buffer.concat([Buffer.from("isom", "latin1"), u32(512), Buffer.from("isom", "latin1")]))
78
+ : Buffer.alloc(0);
79
+ const mdatStart = ftyp.length + moovDraft.length + 8;
80
+ const stcoReal = fullBox("stco", Buffer.concat([u32(1), u32(mdatStart)]));
81
+ const stblReal = box("stbl", Buffer.concat([stsd, stts, stsz, stsc, stcoReal]));
82
+ const moov = box("moov", box("trak", Buffer.concat([
83
+ tkhd,
84
+ box("mdia", Buffer.concat([mdhd, hdlr, box("minf", stblReal)]))
85
+ ])));
86
+ const mdat = box("mdat", Buffer.concat(samples));
87
+ return { file: Buffer.concat([ftyp, moov, mdat]), samples };
88
+ }
89
+
90
+ function readerOver(file) {
91
+ return async (start, end) => {
92
+ const last = Math.min(end, file.length - 1);
93
+ return start > last ? null : file.subarray(start, last + 1);
94
+ };
95
+ }
96
+
97
+ test("a text track's cues are found with their times and their byte ranges", async () => {
98
+ const { file, samples } = buildFile();
99
+
100
+ const plan = await Mp4Container.readSubtitlePlan(readerOver(file), file.length);
101
+
102
+ assert.equal(plan.tracks.length, 1);
103
+ const track = plan.tracks[0];
104
+ assert.equal(track.format, "tx3g");
105
+ assert.equal(track.language, "eng");
106
+ assert.equal(track.samples.length, 2, "the empty sample is a gap, not a cue");
107
+ assert.deepEqual(
108
+ track.samples.map((sample) => [sample.startSeconds, sample.endSeconds]),
109
+ [[0, 2], [4, 6]],
110
+ "times come from the duration table, and the gap keeps its place in it"
111
+ );
112
+ // The bytes the plan points at are the ones that hold the text.
113
+ const first = file.subarray(track.samples[0].offset, track.samples[0].offset + track.samples[0].size);
114
+ assert.equal(Mp4Container.cueTextOf(first, "tx3g"), "First line");
115
+ const second = file.subarray(track.samples[1].offset, track.samples[1].offset + track.samples[1].size);
116
+ assert.equal(Mp4Container.cueTextOf(second, "tx3g"), "Second line");
117
+ assert.equal(second.length, samples[2].length);
118
+ });
119
+
120
+ test("a file with no text track offers nothing", async () => {
121
+ const moov = box("moov", box("trak", box("mdia", Buffer.concat([
122
+ fullBox("mdhd", Buffer.concat([u32(0, 0, 1000, 100), Buffer.from([0, 0, 0, 0])])),
123
+ fullBox("hdlr", Buffer.concat([u32(0), Buffer.from("vide", "latin1"), u32(0, 0, 0)]))
124
+ ]))));
125
+ const file = Buffer.concat([moov, box("mdat", Buffer.alloc(4))]);
126
+
127
+ const plan = await Mp4Container.readSubtitlePlan(readerOver(file), file.length);
128
+
129
+ assert.deepEqual(plan.tracks, []);
130
+ });
131
+
132
+ test("a WebVTT sample gives up the text inside its payload box", () => {
133
+ const payl = box("payl", Buffer.from("Hello there", "utf8"));
134
+ const sample = box("vttc", payl);
135
+
136
+ assert.equal(Mp4Container.cueTextOf(sample, "wvtt"), "Hello there");
137
+ });
138
+
139
+ test("the sample entry's display flags say whether the track is forced", async () => {
140
+ // Apple's QuickTime File Format, "Display flags" under Subtitle sample
141
+ // description: `0x40000000` "Some samples are forced", `0x80000000` "All
142
+ // samples are forced", and setting the second requires the first — together
143
+ // `0xC0000000`. `0x20000000` is vertical placement and says nothing about
144
+ // forcing.
145
+ const readFlags = async (displayFlags) => {
146
+ const { file } = buildFile(displayFlags);
147
+ const plan = await Mp4Container.readSubtitlePlan(readerOver(file), file.length);
148
+ const track = plan.tracks[0];
149
+ return [track.someSamplesForced, track.allSamplesForced];
150
+ };
151
+
152
+ assert.deepEqual(await readFlags(0), [false, false]);
153
+ assert.deepEqual(await readFlags(0x40000000), [true, false]);
154
+ assert.deepEqual(await readFlags(0xc0000000), [true, true]);
155
+ // A writer that set only the second bit still means the track is forced.
156
+ assert.deepEqual(await readFlags(0x80000000), [false, true]);
157
+ // Vertical placement is a different field's business.
158
+ assert.deepEqual(await readFlags(0x20000000), [false, false]);
159
+ });
160
+
161
+ test("a forced sample entry reaches the track the viewer is offered", async () => {
162
+ const forced = buildFile(0xc0000000, true).file;
163
+ const plain = buildFile(0, true).file;
164
+
165
+ const trackOf = async (file) => {
166
+ const container = new Mp4Container({ readRange: readerOver(file), fileSize: file.length });
167
+ const tracks = await container.readTracks();
168
+ return tracks.find((track) => track.type === "subtitle");
169
+ };
170
+
171
+ assert.equal((await trackOf(forced))?.isForced, true);
172
+ assert.equal((await trackOf(plain))?.isForced, false);
173
+ });
@@ -0,0 +1,188 @@
1
+ /**
2
+ * @file The session's one statement of what it has produced.
3
+ *
4
+ * Every check builds real directories and real files, because what is under
5
+ * test is exactly the reading of them: which run's copy wins, what an empty
6
+ * file counts as, and whether a directory that has not changed is read again.
7
+ */
8
+
9
+ import test from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import path from "node:path";
12
+ import os from "node:os";
13
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, unlinkSync } from "node:fs";
14
+
15
+ import { ProducedIndex } from "../services/produced-index.js";
16
+
17
+ /** The naming the fMP4 format uses, reduced to what the index asks of it. */
18
+ const segmentFormat = {
19
+ isSegmentFileName: (name) => /^segment-\d{5}\.mp4$/.test(name),
20
+ segmentIndexFromName: (name) => {
21
+ const match = /^segment-(\d{5})\.mp4$/.exec(name);
22
+ return match ? Number(match[1]) : -1;
23
+ }
24
+ };
25
+
26
+ const nameOf = (index) => `segment-${String(index).padStart(5, "0")}.mp4`;
27
+
28
+ function session() {
29
+ const dirPath = mkdtempSync(path.join(os.tmpdir(), "produced-index-"));
30
+ return {
31
+ dirPath,
32
+ run(number) {
33
+ const dir = path.join(dirPath, `run-${number}`);
34
+ mkdirSync(dir, { recursive: true });
35
+ return {
36
+ dir,
37
+ write(index, contents) {
38
+ writeFileSync(path.join(dir, nameOf(index)), contents);
39
+ return path.join(dir, nameOf(index));
40
+ },
41
+ writeNamed(name, contents) {
42
+ writeFileSync(path.join(dir, name), contents);
43
+ return path.join(dir, name);
44
+ }
45
+ };
46
+ },
47
+ index: () => new ProducedIndex({ dirPath, segmentFormat }),
48
+ remove: () => rmSync(dirPath, { recursive: true, force: true })
49
+ };
50
+ }
51
+
52
+ test("a file with bytes in it is produced; one that was only opened is not", () => {
53
+ const s = session();
54
+ try {
55
+ const run = s.run(1);
56
+ run.write(1, "a piece");
57
+ run.write(2, "");
58
+ const index = s.index();
59
+
60
+ assert.deepEqual([...index.segmentNumbers()], [1]);
61
+ assert.equal(index.pathOf(nameOf(1)), path.join(run.dir, nameOf(1)));
62
+ // The empty file exists and is not an answer — this is the pair of beliefs
63
+ // that stopped playback for ten minutes on 2026-09-03, now one belief.
64
+ assert.equal(index.pathOf(nameOf(2)), null);
65
+ } finally {
66
+ s.remove();
67
+ }
68
+ });
69
+
70
+ test("the newest run's copy wins, whichever run was read last", () => {
71
+ const s = session();
72
+ try {
73
+ const first = s.run(1);
74
+ first.write(7, "old");
75
+ const second = s.run(2);
76
+ second.write(7, "new");
77
+ const index = s.index();
78
+
79
+ assert.equal(index.pathOf(nameOf(7)), path.join(second.dir, nameOf(7)));
80
+
81
+ // Touching the OLDER run must not make it the answer: the rule is applied
82
+ // when the question is asked, not when a directory happens to be read.
83
+ first.write(8, "older still");
84
+ assert.equal(index.pathOf(nameOf(7)), path.join(second.dir, nameOf(7)));
85
+ assert.equal(index.pathOf(nameOf(8)), path.join(first.dir, nameOf(8)));
86
+ } finally {
87
+ s.remove();
88
+ }
89
+ });
90
+
91
+ test("the union of every run is what the session holds", () => {
92
+ const s = session();
93
+ try {
94
+ s.run(1).write(1, "x");
95
+ s.run(2).write(2, "y");
96
+ s.run(3).write(3, "z");
97
+ const index = s.index();
98
+ assert.deepEqual([...index.segmentNumbers()].sort((a, b) => a - b), [1, 2, 3]);
99
+ } finally {
100
+ s.remove();
101
+ }
102
+ });
103
+
104
+ test("a file that is not a segment is found by name", () => {
105
+ const s = session();
106
+ try {
107
+ const run = s.run(1);
108
+ run.writeNamed("init.mp4", "header");
109
+ const index = s.index();
110
+ assert.equal(index.pathOf("init.mp4"), path.join(run.dir, "init.mp4"));
111
+ assert.ok(index.fileNames().includes("init.mp4"));
112
+ } finally {
113
+ s.remove();
114
+ }
115
+ });
116
+
117
+ test("a run whose directory is gone stops answering", () => {
118
+ const s = session();
119
+ try {
120
+ const run = s.run(1);
121
+ run.write(4, "piece");
122
+ const index = s.index();
123
+ assert.equal(index.segmentNumbers().size, 1);
124
+
125
+ rmSync(run.dir, { recursive: true, force: true });
126
+ assert.equal(index.segmentNumbers().size, 0);
127
+ assert.equal(index.pathOf(nameOf(4)), null);
128
+ } finally {
129
+ s.remove();
130
+ }
131
+ });
132
+
133
+ test("a file removed on purpose is not still an answer", () => {
134
+ const s = session();
135
+ try {
136
+ const run = s.run(1);
137
+ const written = run.write(9, "piece");
138
+ const index = s.index();
139
+ assert.equal(index.pathOf(nameOf(9)), written);
140
+
141
+ unlinkSync(written);
142
+ // The directory's own time has moved, but a caller that deletes and asks in
143
+ // the same tick must not be told the file is there.
144
+ index.invalidate();
145
+ assert.equal(index.pathOf(nameOf(9)), null);
146
+ } finally {
147
+ s.remove();
148
+ }
149
+ });
150
+
151
+ test("an unchanged run is not listed again", () => {
152
+ const s = session();
153
+ try {
154
+ const run = s.run(1);
155
+ run.write(1, "piece");
156
+ const index = s.index();
157
+
158
+ index.segmentNumbers();
159
+ const afterFirst = index.directoryReads;
160
+ assert.equal(afterFirst, 1, "the first question reads the run");
161
+
162
+ for (let asked = 0; asked < 20; asked += 1) {
163
+ index.segmentNumbers();
164
+ index.pathOf(nameOf(1));
165
+ }
166
+ assert.equal(
167
+ index.directoryReads,
168
+ afterFirst,
169
+ "forty more questions about a run that has not changed must read nothing"
170
+ );
171
+
172
+ run.write(2, "another piece");
173
+ index.segmentNumbers();
174
+ assert.equal(index.directoryReads, afterFirst + 1, "a run that gained a file is read once more");
175
+ } finally {
176
+ s.remove();
177
+ }
178
+ });
179
+
180
+ test("a session directory that does not exist answers with nothing", () => {
181
+ const index = new ProducedIndex({
182
+ dirPath: path.join(os.tmpdir(), "produced-index-absent-0000"),
183
+ segmentFormat
184
+ });
185
+ assert.deepEqual(index.runDirs(), []);
186
+ assert.equal(index.pathOf(nameOf(1)), null);
187
+ assert.deepEqual([...index.segmentNumbers()], []);
188
+ });