throughline 0.3.1 → 0.3.3
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.
- package/README.md +50 -6
- package/package.json +1 -1
- package/src/token-monitor.mjs +14 -0
- package/src/turn-processor.mjs +283 -272
- package/src/vscode-task.mjs +240 -0
- package/src/vscode-task.test.mjs +520 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync, existsSync, statSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import {
|
|
7
|
+
ensureMonitorTaskFile,
|
|
8
|
+
detectVsCode,
|
|
9
|
+
detectJsoncFeatures,
|
|
10
|
+
detectIndent,
|
|
11
|
+
hasMonitorTask,
|
|
12
|
+
buildMonitorTask,
|
|
13
|
+
} from './vscode-task.mjs';
|
|
14
|
+
|
|
15
|
+
const VSCODE_ENV = { TERM_PROGRAM: 'vscode' };
|
|
16
|
+
const FAKE_BIN = '/fake/abs/path/bin/throughline.mjs';
|
|
17
|
+
|
|
18
|
+
function mkTmpCwd() {
|
|
19
|
+
const dir = mkdtempSync(join(tmpdir(), 'throughline-vscode-'));
|
|
20
|
+
return {
|
|
21
|
+
dir,
|
|
22
|
+
cleanup: () => rmSync(dir, { recursive: true, force: true }),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// --- detectVsCode ---
|
|
27
|
+
|
|
28
|
+
test('detectVsCode: TERM_PROGRAM=vscode is detected', () => {
|
|
29
|
+
assert.equal(detectVsCode({ TERM_PROGRAM: 'vscode' }), true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('detectVsCode: VSCODE_PID is detected', () => {
|
|
33
|
+
assert.equal(detectVsCode({ VSCODE_PID: '123' }), true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('detectVsCode: VSCODE_IPC_HOOK_CLI is detected', () => {
|
|
37
|
+
assert.equal(detectVsCode({ VSCODE_IPC_HOOK_CLI: '/tmp/sock' }), true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('detectVsCode: empty env is not detected', () => {
|
|
41
|
+
assert.equal(detectVsCode({}), false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('detectVsCode: unrelated TERM_PROGRAM is not detected', () => {
|
|
45
|
+
assert.equal(detectVsCode({ TERM_PROGRAM: 'iTerm.app' }), false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// --- detectJsoncFeatures ---
|
|
49
|
+
|
|
50
|
+
test('detectJsoncFeatures: plain JSON is not JSONC', () => {
|
|
51
|
+
assert.equal(detectJsoncFeatures('{"version":"2.0.0","tasks":[]}'), false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('detectJsoncFeatures: line comment is JSONC', () => {
|
|
55
|
+
assert.equal(detectJsoncFeatures('{\n // comment\n "tasks": []\n}'), true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('detectJsoncFeatures: block comment is JSONC', () => {
|
|
59
|
+
assert.equal(detectJsoncFeatures('{\n /* block */\n "tasks": []\n}'), true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('detectJsoncFeatures: trailing comma in array is JSONC', () => {
|
|
63
|
+
assert.equal(detectJsoncFeatures('{"tasks":[1,2,]}'), true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('detectJsoncFeatures: trailing comma in object is JSONC', () => {
|
|
67
|
+
assert.equal(detectJsoncFeatures('{"a":1,}'), true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('detectJsoncFeatures: // inside string literal is not JSONC', () => {
|
|
71
|
+
assert.equal(detectJsoncFeatures('{"url":"http://example.com"}'), false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('detectJsoncFeatures: /* inside string literal is not JSONC', () => {
|
|
75
|
+
assert.equal(detectJsoncFeatures('{"note":"/* not a comment */"}'), false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('detectJsoncFeatures: escaped quote inside string does not confuse scanner', () => {
|
|
79
|
+
assert.equal(detectJsoncFeatures('{"s":"quote\\"inside"}'), false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// --- detectIndent ---
|
|
83
|
+
|
|
84
|
+
test('detectIndent: 2-space indent detected', () => {
|
|
85
|
+
assert.equal(detectIndent('{\n "a": 1\n}'), ' ');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('detectIndent: 4-space indent detected', () => {
|
|
89
|
+
assert.equal(detectIndent('{\n "a": 1\n}'), ' ');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('detectIndent: tab indent detected', () => {
|
|
93
|
+
assert.equal(detectIndent('{\n\t"a": 1\n}'), '\t');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('detectIndent: default to 2 spaces when no indent found', () => {
|
|
97
|
+
assert.equal(detectIndent('{"a":1}'), ' ');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// --- hasMonitorTask ---
|
|
101
|
+
|
|
102
|
+
test('hasMonitorTask: returns true when label matches', () => {
|
|
103
|
+
assert.equal(
|
|
104
|
+
hasMonitorTask({ tasks: [{ label: 'Throughline Monitor' }] }),
|
|
105
|
+
true,
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('hasMonitorTask: returns true when command contains throughline monitor (label renamed)', () => {
|
|
110
|
+
assert.equal(
|
|
111
|
+
hasMonitorTask({
|
|
112
|
+
tasks: [{ label: 'Renamed', command: '/abs/path/throughline', args: ['monitor'] }],
|
|
113
|
+
}),
|
|
114
|
+
true,
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('hasMonitorTask: returns true when args contains throughline monitor', () => {
|
|
119
|
+
assert.equal(
|
|
120
|
+
hasMonitorTask({
|
|
121
|
+
tasks: [{ command: '/usr/bin/node', args: ['/p/bin/throughline.mjs', 'monitor'] }],
|
|
122
|
+
}),
|
|
123
|
+
true,
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('hasMonitorTask: returns false for unrelated tasks', () => {
|
|
128
|
+
assert.equal(
|
|
129
|
+
hasMonitorTask({ tasks: [{ label: 'Build', command: 'make' }] }),
|
|
130
|
+
false,
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('hasMonitorTask: handles missing tasks array', () => {
|
|
135
|
+
assert.equal(hasMonitorTask({}), false);
|
|
136
|
+
assert.equal(hasMonitorTask({ tasks: null }), false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// --- buildMonitorTask ---
|
|
140
|
+
|
|
141
|
+
test('buildMonitorTask: uses type=process with provided bin as args[0]', () => {
|
|
142
|
+
const task = buildMonitorTask('/abs/bin/throughline.mjs');
|
|
143
|
+
assert.equal(task.label, 'Throughline Monitor');
|
|
144
|
+
assert.equal(task.type, 'process');
|
|
145
|
+
assert.equal(task.args[0], '/abs/bin/throughline.mjs');
|
|
146
|
+
assert.deepEqual(task.args.slice(1), ['monitor']);
|
|
147
|
+
assert.equal(task.runOptions.runOn, 'folderOpen');
|
|
148
|
+
assert.equal(task.isBackground, true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// --- ensureMonitorTaskFile: skip conditions ---
|
|
152
|
+
|
|
153
|
+
test('ensureMonitorTaskFile: opt_out via THROUGHLINE_NO_VSCODE=1', () => {
|
|
154
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
155
|
+
try {
|
|
156
|
+
const result = ensureMonitorTaskFile({
|
|
157
|
+
cwd: dir,
|
|
158
|
+
env: { ...VSCODE_ENV, THROUGHLINE_NO_VSCODE: '1' },
|
|
159
|
+
throughlineBin: FAKE_BIN,
|
|
160
|
+
});
|
|
161
|
+
assert.equal(result.action, 'skipped');
|
|
162
|
+
assert.equal(result.reason, 'opt_out');
|
|
163
|
+
assert.equal(existsSync(join(dir, '.vscode', 'tasks.json')), false);
|
|
164
|
+
} finally {
|
|
165
|
+
cleanup();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('ensureMonitorTaskFile: no_cwd when cwd does not exist', () => {
|
|
170
|
+
const result = ensureMonitorTaskFile({
|
|
171
|
+
cwd: '/definitely/does/not/exist/xyz123',
|
|
172
|
+
env: VSCODE_ENV,
|
|
173
|
+
throughlineBin: FAKE_BIN,
|
|
174
|
+
});
|
|
175
|
+
assert.equal(result.action, 'skipped');
|
|
176
|
+
assert.equal(result.reason, 'no_cwd');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('ensureMonitorTaskFile: no_cwd when cwd is missing', () => {
|
|
180
|
+
const result = ensureMonitorTaskFile({
|
|
181
|
+
env: VSCODE_ENV,
|
|
182
|
+
throughlineBin: FAKE_BIN,
|
|
183
|
+
});
|
|
184
|
+
assert.equal(result.action, 'skipped');
|
|
185
|
+
assert.equal(result.reason, 'no_cwd');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('ensureMonitorTaskFile: not_vscode when no VSCode env vars', () => {
|
|
189
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
190
|
+
try {
|
|
191
|
+
const result = ensureMonitorTaskFile({
|
|
192
|
+
cwd: dir,
|
|
193
|
+
env: {},
|
|
194
|
+
throughlineBin: FAKE_BIN,
|
|
195
|
+
});
|
|
196
|
+
assert.equal(result.action, 'skipped');
|
|
197
|
+
assert.equal(result.reason, 'not_vscode');
|
|
198
|
+
} finally {
|
|
199
|
+
cleanup();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('ensureMonitorTaskFile: no_bin when throughlineBin is empty', () => {
|
|
204
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
205
|
+
try {
|
|
206
|
+
const result = ensureMonitorTaskFile({
|
|
207
|
+
cwd: dir,
|
|
208
|
+
env: VSCODE_ENV,
|
|
209
|
+
throughlineBin: '',
|
|
210
|
+
});
|
|
211
|
+
assert.equal(result.action, 'skipped');
|
|
212
|
+
assert.equal(result.reason, 'no_bin');
|
|
213
|
+
} finally {
|
|
214
|
+
cleanup();
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// --- ensureMonitorTaskFile: create path ---
|
|
219
|
+
|
|
220
|
+
test('ensureMonitorTaskFile: created when .vscode/ missing', () => {
|
|
221
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
222
|
+
try {
|
|
223
|
+
const result = ensureMonitorTaskFile({
|
|
224
|
+
cwd: dir,
|
|
225
|
+
env: VSCODE_ENV,
|
|
226
|
+
throughlineBin: FAKE_BIN,
|
|
227
|
+
});
|
|
228
|
+
assert.equal(result.action, 'created');
|
|
229
|
+
const tasksPath = join(dir, '.vscode', 'tasks.json');
|
|
230
|
+
assert.equal(existsSync(tasksPath), true);
|
|
231
|
+
const obj = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
|
232
|
+
assert.equal(obj.version, '2.0.0');
|
|
233
|
+
assert.equal(obj.tasks.length, 1);
|
|
234
|
+
assert.equal(obj.tasks[0].label, 'Throughline Monitor');
|
|
235
|
+
assert.equal(obj.tasks[0].type, 'process');
|
|
236
|
+
assert.equal(obj.tasks[0].args[0], FAKE_BIN);
|
|
237
|
+
} finally {
|
|
238
|
+
cleanup();
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('ensureMonitorTaskFile: created when .vscode/ exists but tasks.json missing', () => {
|
|
243
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
244
|
+
try {
|
|
245
|
+
mkdirSync(join(dir, '.vscode'));
|
|
246
|
+
const result = ensureMonitorTaskFile({
|
|
247
|
+
cwd: dir,
|
|
248
|
+
env: VSCODE_ENV,
|
|
249
|
+
throughlineBin: FAKE_BIN,
|
|
250
|
+
});
|
|
251
|
+
assert.equal(result.action, 'created');
|
|
252
|
+
assert.equal(existsSync(join(dir, '.vscode', 'tasks.json')), true);
|
|
253
|
+
} finally {
|
|
254
|
+
cleanup();
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// --- ensureMonitorTaskFile: merge path ---
|
|
259
|
+
|
|
260
|
+
test('ensureMonitorTaskFile: merged preserves existing tasks and version', () => {
|
|
261
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
262
|
+
try {
|
|
263
|
+
mkdirSync(join(dir, '.vscode'));
|
|
264
|
+
const existing = {
|
|
265
|
+
version: '2.0.0',
|
|
266
|
+
tasks: [
|
|
267
|
+
{ label: 'Build', type: 'shell', command: 'make' },
|
|
268
|
+
{ label: 'Test', type: 'shell', command: 'make test' },
|
|
269
|
+
],
|
|
270
|
+
};
|
|
271
|
+
writeFileSync(join(dir, '.vscode', 'tasks.json'), JSON.stringify(existing, null, 2));
|
|
272
|
+
const result = ensureMonitorTaskFile({
|
|
273
|
+
cwd: dir,
|
|
274
|
+
env: VSCODE_ENV,
|
|
275
|
+
throughlineBin: FAKE_BIN,
|
|
276
|
+
});
|
|
277
|
+
assert.equal(result.action, 'merged');
|
|
278
|
+
const obj = JSON.parse(readFileSync(join(dir, '.vscode', 'tasks.json'), 'utf8'));
|
|
279
|
+
assert.equal(obj.version, '2.0.0');
|
|
280
|
+
assert.equal(obj.tasks.length, 3);
|
|
281
|
+
assert.equal(obj.tasks[0].label, 'Build');
|
|
282
|
+
assert.equal(obj.tasks[1].label, 'Test');
|
|
283
|
+
assert.equal(obj.tasks[2].label, 'Throughline Monitor');
|
|
284
|
+
} finally {
|
|
285
|
+
cleanup();
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('ensureMonitorTaskFile: merged sets version when missing', () => {
|
|
290
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
291
|
+
try {
|
|
292
|
+
mkdirSync(join(dir, '.vscode'));
|
|
293
|
+
writeFileSync(join(dir, '.vscode', 'tasks.json'), JSON.stringify({ tasks: [] }));
|
|
294
|
+
const result = ensureMonitorTaskFile({
|
|
295
|
+
cwd: dir,
|
|
296
|
+
env: VSCODE_ENV,
|
|
297
|
+
throughlineBin: FAKE_BIN,
|
|
298
|
+
});
|
|
299
|
+
assert.equal(result.action, 'merged');
|
|
300
|
+
const obj = JSON.parse(readFileSync(join(dir, '.vscode', 'tasks.json'), 'utf8'));
|
|
301
|
+
assert.equal(obj.version, '2.0.0');
|
|
302
|
+
assert.equal(obj.tasks.length, 1);
|
|
303
|
+
} finally {
|
|
304
|
+
cleanup();
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('ensureMonitorTaskFile: merged preserves indent style (4 spaces)', () => {
|
|
309
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
310
|
+
try {
|
|
311
|
+
mkdirSync(join(dir, '.vscode'));
|
|
312
|
+
const existing = { version: '2.0.0', tasks: [{ label: 'Build' }] };
|
|
313
|
+
writeFileSync(
|
|
314
|
+
join(dir, '.vscode', 'tasks.json'),
|
|
315
|
+
JSON.stringify(existing, null, 4),
|
|
316
|
+
);
|
|
317
|
+
const result = ensureMonitorTaskFile({
|
|
318
|
+
cwd: dir,
|
|
319
|
+
env: VSCODE_ENV,
|
|
320
|
+
throughlineBin: FAKE_BIN,
|
|
321
|
+
});
|
|
322
|
+
assert.equal(result.action, 'merged');
|
|
323
|
+
const text = readFileSync(join(dir, '.vscode', 'tasks.json'), 'utf8');
|
|
324
|
+
assert.match(text, /^ "version"/m);
|
|
325
|
+
} finally {
|
|
326
|
+
cleanup();
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// --- ensureMonitorTaskFile: already_present ---
|
|
331
|
+
|
|
332
|
+
test('ensureMonitorTaskFile: already_present when label matches', () => {
|
|
333
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
334
|
+
try {
|
|
335
|
+
mkdirSync(join(dir, '.vscode'));
|
|
336
|
+
const existing = {
|
|
337
|
+
version: '2.0.0',
|
|
338
|
+
tasks: [{ label: 'Throughline Monitor', command: 'foo' }],
|
|
339
|
+
};
|
|
340
|
+
const tasksPath = join(dir, '.vscode', 'tasks.json');
|
|
341
|
+
writeFileSync(tasksPath, JSON.stringify(existing, null, 2));
|
|
342
|
+
const beforeMtime = statSync(tasksPath).mtimeMs;
|
|
343
|
+
|
|
344
|
+
const result = ensureMonitorTaskFile({
|
|
345
|
+
cwd: dir,
|
|
346
|
+
env: VSCODE_ENV,
|
|
347
|
+
throughlineBin: FAKE_BIN,
|
|
348
|
+
});
|
|
349
|
+
assert.equal(result.action, 'already_present');
|
|
350
|
+
|
|
351
|
+
const afterMtime = statSync(tasksPath).mtimeMs;
|
|
352
|
+
assert.equal(beforeMtime, afterMtime);
|
|
353
|
+
} finally {
|
|
354
|
+
cleanup();
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test('ensureMonitorTaskFile: already_present when command references throughline monitor (label renamed)', () => {
|
|
359
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
360
|
+
try {
|
|
361
|
+
mkdirSync(join(dir, '.vscode'));
|
|
362
|
+
const existing = {
|
|
363
|
+
version: '2.0.0',
|
|
364
|
+
tasks: [
|
|
365
|
+
{
|
|
366
|
+
label: 'My Custom Monitor',
|
|
367
|
+
type: 'process',
|
|
368
|
+
command: '/usr/bin/node',
|
|
369
|
+
args: ['/path/to/bin/throughline.mjs', 'monitor'],
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
};
|
|
373
|
+
writeFileSync(join(dir, '.vscode', 'tasks.json'), JSON.stringify(existing, null, 2));
|
|
374
|
+
|
|
375
|
+
const result = ensureMonitorTaskFile({
|
|
376
|
+
cwd: dir,
|
|
377
|
+
env: VSCODE_ENV,
|
|
378
|
+
throughlineBin: FAKE_BIN,
|
|
379
|
+
});
|
|
380
|
+
assert.equal(result.action, 'already_present');
|
|
381
|
+
} finally {
|
|
382
|
+
cleanup();
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test('ensureMonitorTaskFile: second call is idempotent (already_present after created)', () => {
|
|
387
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
388
|
+
try {
|
|
389
|
+
const first = ensureMonitorTaskFile({
|
|
390
|
+
cwd: dir,
|
|
391
|
+
env: VSCODE_ENV,
|
|
392
|
+
throughlineBin: FAKE_BIN,
|
|
393
|
+
});
|
|
394
|
+
assert.equal(first.action, 'created');
|
|
395
|
+
|
|
396
|
+
const tasksPath = join(dir, '.vscode', 'tasks.json');
|
|
397
|
+
const mtimeAfterCreate = statSync(tasksPath).mtimeMs;
|
|
398
|
+
|
|
399
|
+
const second = ensureMonitorTaskFile({
|
|
400
|
+
cwd: dir,
|
|
401
|
+
env: VSCODE_ENV,
|
|
402
|
+
throughlineBin: FAKE_BIN,
|
|
403
|
+
});
|
|
404
|
+
assert.equal(second.action, 'already_present');
|
|
405
|
+
|
|
406
|
+
const mtimeAfterSecond = statSync(tasksPath).mtimeMs;
|
|
407
|
+
assert.equal(mtimeAfterCreate, mtimeAfterSecond);
|
|
408
|
+
} finally {
|
|
409
|
+
cleanup();
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// --- ensureMonitorTaskFile: JSONC ---
|
|
414
|
+
|
|
415
|
+
test('ensureMonitorTaskFile: jsonc_unsupported for file with line comments', () => {
|
|
416
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
417
|
+
try {
|
|
418
|
+
mkdirSync(join(dir, '.vscode'));
|
|
419
|
+
const content = '{\n // VSCode style comment\n "version": "2.0.0",\n "tasks": []\n}';
|
|
420
|
+
const tasksPath = join(dir, '.vscode', 'tasks.json');
|
|
421
|
+
writeFileSync(tasksPath, content);
|
|
422
|
+
|
|
423
|
+
const result = ensureMonitorTaskFile({
|
|
424
|
+
cwd: dir,
|
|
425
|
+
env: VSCODE_ENV,
|
|
426
|
+
throughlineBin: FAKE_BIN,
|
|
427
|
+
});
|
|
428
|
+
assert.equal(result.action, 'skipped');
|
|
429
|
+
assert.equal(result.reason, 'jsonc_unsupported');
|
|
430
|
+
|
|
431
|
+
assert.equal(readFileSync(tasksPath, 'utf8'), content);
|
|
432
|
+
assert.equal(existsSync(join(dir, '.vscode', '.throughline-jsonc-noted')), true);
|
|
433
|
+
} finally {
|
|
434
|
+
cleanup();
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test('ensureMonitorTaskFile: jsonc_unsupported for file with trailing commas', () => {
|
|
439
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
440
|
+
try {
|
|
441
|
+
mkdirSync(join(dir, '.vscode'));
|
|
442
|
+
const content = '{\n "version": "2.0.0",\n "tasks": [],\n}';
|
|
443
|
+
writeFileSync(join(dir, '.vscode', 'tasks.json'), content);
|
|
444
|
+
|
|
445
|
+
const result = ensureMonitorTaskFile({
|
|
446
|
+
cwd: dir,
|
|
447
|
+
env: VSCODE_ENV,
|
|
448
|
+
throughlineBin: FAKE_BIN,
|
|
449
|
+
});
|
|
450
|
+
assert.equal(result.action, 'skipped');
|
|
451
|
+
assert.equal(result.reason, 'jsonc_unsupported');
|
|
452
|
+
} finally {
|
|
453
|
+
cleanup();
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test('ensureMonitorTaskFile: jsonc_unsupported marker suppresses stderr on 2nd call', () => {
|
|
458
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
459
|
+
try {
|
|
460
|
+
mkdirSync(join(dir, '.vscode'));
|
|
461
|
+
writeFileSync(
|
|
462
|
+
join(dir, '.vscode', 'tasks.json'),
|
|
463
|
+
'{\n // JSONC\n "tasks": []\n}',
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
const captured = [];
|
|
467
|
+
const origWrite = process.stderr.write.bind(process.stderr);
|
|
468
|
+
process.stderr.write = (chunk) => {
|
|
469
|
+
captured.push(String(chunk));
|
|
470
|
+
return true;
|
|
471
|
+
};
|
|
472
|
+
try {
|
|
473
|
+
const r1 = ensureMonitorTaskFile({
|
|
474
|
+
cwd: dir,
|
|
475
|
+
env: VSCODE_ENV,
|
|
476
|
+
throughlineBin: FAKE_BIN,
|
|
477
|
+
});
|
|
478
|
+
assert.equal(r1.action, 'skipped');
|
|
479
|
+
assert.equal(r1.reason, 'jsonc_unsupported');
|
|
480
|
+
const firstCount = captured.length;
|
|
481
|
+
assert.ok(firstCount > 0, 'first call should emit guidance');
|
|
482
|
+
|
|
483
|
+
const r2 = ensureMonitorTaskFile({
|
|
484
|
+
cwd: dir,
|
|
485
|
+
env: VSCODE_ENV,
|
|
486
|
+
throughlineBin: FAKE_BIN,
|
|
487
|
+
});
|
|
488
|
+
assert.equal(r2.action, 'skipped');
|
|
489
|
+
assert.equal(r2.reason, 'jsonc_unsupported');
|
|
490
|
+
assert.equal(captured.length, firstCount, 'second call should be silent');
|
|
491
|
+
} finally {
|
|
492
|
+
process.stderr.write = origWrite;
|
|
493
|
+
}
|
|
494
|
+
} finally {
|
|
495
|
+
cleanup();
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// --- ensureMonitorTaskFile: parse errors ---
|
|
500
|
+
|
|
501
|
+
test('ensureMonitorTaskFile: parse_error for malformed JSON', () => {
|
|
502
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
503
|
+
try {
|
|
504
|
+
mkdirSync(join(dir, '.vscode'));
|
|
505
|
+
const content = '{"tasks":[broken';
|
|
506
|
+
const tasksPath = join(dir, '.vscode', 'tasks.json');
|
|
507
|
+
writeFileSync(tasksPath, content);
|
|
508
|
+
|
|
509
|
+
const result = ensureMonitorTaskFile({
|
|
510
|
+
cwd: dir,
|
|
511
|
+
env: VSCODE_ENV,
|
|
512
|
+
throughlineBin: FAKE_BIN,
|
|
513
|
+
});
|
|
514
|
+
assert.equal(result.action, 'skipped');
|
|
515
|
+
assert.equal(result.reason, 'parse_error');
|
|
516
|
+
assert.equal(readFileSync(tasksPath, 'utf8'), content);
|
|
517
|
+
} finally {
|
|
518
|
+
cleanup();
|
|
519
|
+
}
|
|
520
|
+
});
|