throughline 0.3.21 → 0.3.23
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.ja.md +255 -0
- package/README.md +121 -16
- package/bin/throughline.mjs +0 -0
- package/package.json +1 -1
- package/src/cli/install.mjs +54 -2
- package/src/cli/install.test.mjs +72 -1
- package/src/turn-processor.mjs +304 -304
- package/src/vscode-task.mjs +95 -22
- package/src/vscode-task.test.mjs +233 -2
package/src/vscode-task.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, statSync } from 'node:fs';
|
|
17
17
|
import { fileURLToPath } from 'node:url';
|
|
18
|
-
import { join, dirname } from 'node:path';
|
|
18
|
+
import { join, dirname, isAbsolute } from 'node:path';
|
|
19
19
|
|
|
20
20
|
const MONITOR_LABEL = 'Throughline Monitor';
|
|
21
21
|
const JSONC_MARKER_FILENAME = '.throughline-jsonc-noted';
|
|
@@ -81,22 +81,58 @@ export function detectIndent(text) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
|
-
* tasks
|
|
84
|
+
* tasks 配列内で Throughline Monitor 相当のタスクの index を返す。
|
|
85
85
|
* ユーザーが label をリネームしても command/args に "throughline" + "monitor" が
|
|
86
|
-
*
|
|
86
|
+
* あれば同一タスクとみなす。見つからない場合は -1。
|
|
87
87
|
*/
|
|
88
|
-
export function
|
|
88
|
+
export function findMonitorTaskIndex(obj) {
|
|
89
89
|
const tasks = obj?.tasks;
|
|
90
|
-
if (!Array.isArray(tasks)) return
|
|
91
|
-
for (
|
|
90
|
+
if (!Array.isArray(tasks)) return -1;
|
|
91
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
92
|
+
const t = tasks[i];
|
|
92
93
|
if (!t || typeof t !== 'object') continue;
|
|
93
|
-
if (t.label === MONITOR_LABEL) return
|
|
94
|
+
if (t.label === MONITOR_LABEL) return i;
|
|
94
95
|
const invocation = [t.command, ...(Array.isArray(t.args) ? t.args : [])]
|
|
95
96
|
.filter((s) => typeof s === 'string')
|
|
96
97
|
.join(' ')
|
|
97
98
|
.toLowerCase();
|
|
98
99
|
if (invocation.includes('throughline') && invocation.includes('monitor')) {
|
|
99
|
-
return
|
|
100
|
+
return i;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return -1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function hasMonitorTask(obj) {
|
|
107
|
+
return findMonitorTaskIndex(obj) >= 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 既存の Monitor タスクが「別環境(別 OS / 別 install 場所)の絶対パスを参照していて
|
|
112
|
+
* 現在の環境では起動できない」状態かを判定する。
|
|
113
|
+
*
|
|
114
|
+
* 判定基準:
|
|
115
|
+
* - command が絶対パスかつ existsSync(command) === false → 壊れている
|
|
116
|
+
* - args の中に絶対パスの .mjs / .js / .cjs があり、それが存在しない → 壊れている
|
|
117
|
+
*
|
|
118
|
+
* 相対パス("node" / "throughline" 等の PATH 解決前提)は判定対象外。
|
|
119
|
+
* ユーザーが手動で PATH 解決型に書き換えたケースを誤って上書きしないため。
|
|
120
|
+
*/
|
|
121
|
+
export function isMonitorTaskBroken(task) {
|
|
122
|
+
if (!task || typeof task !== 'object') return false;
|
|
123
|
+
if (typeof task.command === 'string' && isAbsolute(task.command) && !existsSync(task.command)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (Array.isArray(task.args)) {
|
|
127
|
+
for (const a of task.args) {
|
|
128
|
+
if (
|
|
129
|
+
typeof a === 'string' &&
|
|
130
|
+
isAbsolute(a) &&
|
|
131
|
+
/\.(mjs|js|cjs)$/.test(a) &&
|
|
132
|
+
!existsSync(a)
|
|
133
|
+
) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
100
136
|
}
|
|
101
137
|
}
|
|
102
138
|
return false;
|
|
@@ -173,18 +209,33 @@ function atomicWrite(targetPath, contents) {
|
|
|
173
209
|
* セットアップ済みのため沈黙する(冪等性)。
|
|
174
210
|
*/
|
|
175
211
|
export function buildSetupNotice(action) {
|
|
176
|
-
if (action
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
212
|
+
if (action === 'created' || action === 'merged') {
|
|
213
|
+
return (
|
|
214
|
+
'<system-reminder>\n' +
|
|
215
|
+
'Throughline: このプロジェクトに token-monitor の自動起動設定を追加しました (.vscode/tasks.json)。\n' +
|
|
216
|
+
'今すぐ monitor を起動するには、VSCode のコマンドパレット (Ctrl+Shift+P) から\n' +
|
|
217
|
+
' Developer: Reload Window\n' +
|
|
218
|
+
'を実行してください。次回以降はフォルダを開いた瞬間に自動起動します。\n' +
|
|
219
|
+
'\n' +
|
|
220
|
+
'このお知らせは今このプロジェクトで初回セットアップが完了したためだけに 1 度だけ流れています。\n' +
|
|
221
|
+
'必ずユーザー(あなたの対話相手)に日本語でそのまま伝えてください。\n' +
|
|
222
|
+
'</system-reminder>\n'
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
if (action === 'repaired') {
|
|
226
|
+
return (
|
|
227
|
+
'<system-reminder>\n' +
|
|
228
|
+
'Throughline: .vscode/tasks.json に登録されていた token-monitor タスクが\n' +
|
|
229
|
+
'別環境(別 OS / 別 install 場所)の絶対パスを参照していたため、現環境向けに自動修復しました。\n' +
|
|
230
|
+
'反映するには VSCode のコマンドパレット (Ctrl+Shift+P) から\n' +
|
|
231
|
+
' Developer: Reload Window\n' +
|
|
232
|
+
'を実行してください。\n' +
|
|
233
|
+
'\n' +
|
|
234
|
+
'必ずユーザー(あなたの対話相手)に日本語でそのまま伝えてください。\n' +
|
|
235
|
+
'</system-reminder>\n'
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
188
239
|
}
|
|
189
240
|
|
|
190
241
|
function emitSetupNotice(action) {
|
|
@@ -211,9 +262,13 @@ function emitJsoncGuidanceOnce(vscodeDir) {
|
|
|
211
262
|
/**
|
|
212
263
|
* VSCode の tasks.json に Throughline Monitor タスクを(必要なら)自動登録する。
|
|
213
264
|
*
|
|
265
|
+
* 既存タスクが別環境の絶対パスを参照していて現環境で起動できない場合は
|
|
266
|
+
* command/args だけを差し替えて修復する(label / presentation 等のユーザー
|
|
267
|
+
* カスタマイズは保持)。
|
|
268
|
+
*
|
|
214
269
|
* @param {{cwd?: string, env?: NodeJS.ProcessEnv, throughlineBin?: string | null}} opts
|
|
215
270
|
* @returns {{
|
|
216
|
-
* action: 'created' | 'merged' | 'already_present' | 'skipped',
|
|
271
|
+
* action: 'created' | 'merged' | 'repaired' | 'already_present' | 'skipped',
|
|
217
272
|
* reason?: string,
|
|
218
273
|
* path?: string,
|
|
219
274
|
* }}
|
|
@@ -264,7 +319,25 @@ export function ensureMonitorTaskFile(opts = {}) {
|
|
|
264
319
|
return { action: 'skipped', reason: 'parse_error', path: tasksPath };
|
|
265
320
|
}
|
|
266
321
|
|
|
267
|
-
|
|
322
|
+
const existingIdx = findMonitorTaskIndex(obj);
|
|
323
|
+
if (existingIdx >= 0) {
|
|
324
|
+
const existing = obj.tasks[existingIdx];
|
|
325
|
+
if (isMonitorTaskBroken(existing)) {
|
|
326
|
+
const fresh = buildMonitorTask(bin);
|
|
327
|
+
const repaired = {
|
|
328
|
+
...existing,
|
|
329
|
+
type: fresh.type,
|
|
330
|
+
command: fresh.command,
|
|
331
|
+
args: fresh.args,
|
|
332
|
+
};
|
|
333
|
+
const indent = detectIndent(text);
|
|
334
|
+
const nextTasks = [...obj.tasks];
|
|
335
|
+
nextTasks[existingIdx] = repaired;
|
|
336
|
+
const nextObj = { ...obj, version: obj.version ?? '2.0.0', tasks: nextTasks };
|
|
337
|
+
atomicWrite(tasksPath, JSON.stringify(nextObj, null, indent) + '\n');
|
|
338
|
+
emitSetupNotice('repaired');
|
|
339
|
+
return { action: 'repaired', path: tasksPath };
|
|
340
|
+
}
|
|
268
341
|
return { action: 'already_present', path: tasksPath };
|
|
269
342
|
}
|
|
270
343
|
|
package/src/vscode-task.test.mjs
CHANGED
|
@@ -9,12 +9,16 @@ import {
|
|
|
9
9
|
detectJsoncFeatures,
|
|
10
10
|
detectIndent,
|
|
11
11
|
hasMonitorTask,
|
|
12
|
+
findMonitorTaskIndex,
|
|
13
|
+
isMonitorTaskBroken,
|
|
12
14
|
buildMonitorTask,
|
|
13
15
|
buildSetupNotice,
|
|
14
16
|
} from './vscode-task.mjs';
|
|
15
17
|
|
|
16
18
|
const VSCODE_ENV = { TERM_PROGRAM: 'vscode' };
|
|
17
|
-
|
|
19
|
+
// 実在する絶対パスを使う。`isMonitorTaskBroken` が「絶対パス + 非存在」で broken 判定するので、
|
|
20
|
+
// 架空パスを使うと意図せず repaired ブランチに落ちてしまう。
|
|
21
|
+
const FAKE_BIN = process.execPath;
|
|
18
22
|
|
|
19
23
|
function mkTmpCwd() {
|
|
20
24
|
const dir = mkdtempSync(join(tmpdir(), 'throughline-vscode-'));
|
|
@@ -137,6 +141,71 @@ test('hasMonitorTask: handles missing tasks array', () => {
|
|
|
137
141
|
assert.equal(hasMonitorTask({ tasks: null }), false);
|
|
138
142
|
});
|
|
139
143
|
|
|
144
|
+
// --- findMonitorTaskIndex ---
|
|
145
|
+
|
|
146
|
+
test('findMonitorTaskIndex: returns index when label matches', () => {
|
|
147
|
+
assert.equal(
|
|
148
|
+
findMonitorTaskIndex({ tasks: [{ label: 'Build' }, { label: 'Throughline Monitor' }] }),
|
|
149
|
+
1,
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('findMonitorTaskIndex: returns -1 when no match', () => {
|
|
154
|
+
assert.equal(findMonitorTaskIndex({ tasks: [{ label: 'Build' }] }), -1);
|
|
155
|
+
assert.equal(findMonitorTaskIndex({}), -1);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// --- isMonitorTaskBroken ---
|
|
159
|
+
|
|
160
|
+
test('isMonitorTaskBroken: false when command is an existing absolute path', () => {
|
|
161
|
+
assert.equal(
|
|
162
|
+
isMonitorTaskBroken({ command: process.execPath, args: ['monitor'] }),
|
|
163
|
+
false,
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('isMonitorTaskBroken: true when command is a non-existent absolute path', () => {
|
|
168
|
+
assert.equal(
|
|
169
|
+
isMonitorTaskBroken({ command: '/definitely/does/not/exist/node', args: ['monitor'] }),
|
|
170
|
+
true,
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('isMonitorTaskBroken: false when command is a relative name (PATH-resolved)', () => {
|
|
175
|
+
// ユーザーが手動で "node" / "throughline" に書き換えたケースは誤上書きしない
|
|
176
|
+
assert.equal(
|
|
177
|
+
isMonitorTaskBroken({ command: 'node', args: ['/x/throughline.mjs', 'monitor'] }),
|
|
178
|
+
true, // args 側の絶対パスが壊れているので true
|
|
179
|
+
);
|
|
180
|
+
assert.equal(
|
|
181
|
+
isMonitorTaskBroken({ command: 'throughline', args: ['monitor'] }),
|
|
182
|
+
false,
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('isMonitorTaskBroken: true when args contains non-existent absolute .mjs path', () => {
|
|
187
|
+
assert.equal(
|
|
188
|
+
isMonitorTaskBroken({
|
|
189
|
+
command: process.execPath,
|
|
190
|
+
args: ['/no/such/file/throughline.mjs', 'monitor'],
|
|
191
|
+
}),
|
|
192
|
+
true,
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('isMonitorTaskBroken: false when args has only relative strings', () => {
|
|
197
|
+
assert.equal(
|
|
198
|
+
isMonitorTaskBroken({ command: process.execPath, args: ['monitor'] }),
|
|
199
|
+
false,
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('isMonitorTaskBroken: handles malformed task safely', () => {
|
|
204
|
+
assert.equal(isMonitorTaskBroken(null), false);
|
|
205
|
+
assert.equal(isMonitorTaskBroken({}), false);
|
|
206
|
+
assert.equal(isMonitorTaskBroken({ command: 42 }), false);
|
|
207
|
+
});
|
|
208
|
+
|
|
140
209
|
// --- buildMonitorTask ---
|
|
141
210
|
|
|
142
211
|
test('buildMonitorTask: uses type=shell with provided bin as args[0] for PTY allocation', () => {
|
|
@@ -367,7 +436,8 @@ test('ensureMonitorTaskFile: already_present when command references throughline
|
|
|
367
436
|
label: 'My Custom Monitor',
|
|
368
437
|
type: 'process',
|
|
369
438
|
command: '/usr/bin/node',
|
|
370
|
-
|
|
439
|
+
// 相対パスにして broken 判定を避ける(このテストは「label renamed でも検出できるか」だけが論点)
|
|
440
|
+
args: ['./throughline.mjs', 'monitor'],
|
|
371
441
|
},
|
|
372
442
|
],
|
|
373
443
|
};
|
|
@@ -411,6 +481,160 @@ test('ensureMonitorTaskFile: second call is idempotent (already_present after cr
|
|
|
411
481
|
}
|
|
412
482
|
});
|
|
413
483
|
|
|
484
|
+
// --- ensureMonitorTaskFile: cross-environment repair (地雷 4) ---
|
|
485
|
+
|
|
486
|
+
test('ensureMonitorTaskFile: repaired when existing task points to non-existent absolute paths', () => {
|
|
487
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
488
|
+
try {
|
|
489
|
+
mkdirSync(join(dir, '.vscode'));
|
|
490
|
+
// 別 OS で生成されたタスク: command と args の絶対パスが現環境には存在しない
|
|
491
|
+
const stale = {
|
|
492
|
+
version: '2.0.0',
|
|
493
|
+
tasks: [
|
|
494
|
+
{
|
|
495
|
+
label: 'Throughline Monitor',
|
|
496
|
+
type: 'shell',
|
|
497
|
+
command: '/old/env/node',
|
|
498
|
+
args: ['/old/env/throughline.mjs', 'monitor'],
|
|
499
|
+
presentation: { panel: 'dedicated', group: 'throughline' },
|
|
500
|
+
isBackground: true,
|
|
501
|
+
},
|
|
502
|
+
],
|
|
503
|
+
};
|
|
504
|
+
const tasksPath = join(dir, '.vscode', 'tasks.json');
|
|
505
|
+
writeFileSync(tasksPath, JSON.stringify(stale, null, 2));
|
|
506
|
+
|
|
507
|
+
const result = ensureMonitorTaskFile({
|
|
508
|
+
cwd: dir,
|
|
509
|
+
env: VSCODE_ENV,
|
|
510
|
+
throughlineBin: FAKE_BIN,
|
|
511
|
+
});
|
|
512
|
+
assert.equal(result.action, 'repaired');
|
|
513
|
+
|
|
514
|
+
const obj = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
|
515
|
+
assert.equal(obj.tasks.length, 1);
|
|
516
|
+
const task = obj.tasks[0];
|
|
517
|
+
// command と args は現環境向けに差し替わる
|
|
518
|
+
assert.equal(task.command, process.execPath);
|
|
519
|
+
assert.deepEqual(task.args, [FAKE_BIN, 'monitor']);
|
|
520
|
+
// ユーザーカスタマイズ (presentation 等) は保持される
|
|
521
|
+
assert.equal(task.label, 'Throughline Monitor');
|
|
522
|
+
assert.deepEqual(task.presentation, { panel: 'dedicated', group: 'throughline' });
|
|
523
|
+
assert.equal(task.isBackground, true);
|
|
524
|
+
} finally {
|
|
525
|
+
cleanup();
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test('ensureMonitorTaskFile: repaired preserves other tasks in the file', () => {
|
|
530
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
531
|
+
try {
|
|
532
|
+
mkdirSync(join(dir, '.vscode'));
|
|
533
|
+
const stale = {
|
|
534
|
+
version: '2.0.0',
|
|
535
|
+
tasks: [
|
|
536
|
+
{ label: 'Build', type: 'shell', command: 'make' },
|
|
537
|
+
{
|
|
538
|
+
label: 'Throughline Monitor',
|
|
539
|
+
type: 'shell',
|
|
540
|
+
command: '/old/env/node',
|
|
541
|
+
args: ['/old/env/throughline.mjs', 'monitor'],
|
|
542
|
+
},
|
|
543
|
+
{ label: 'Test', type: 'shell', command: 'npm test' },
|
|
544
|
+
],
|
|
545
|
+
};
|
|
546
|
+
writeFileSync(join(dir, '.vscode', 'tasks.json'), JSON.stringify(stale, null, 2));
|
|
547
|
+
|
|
548
|
+
const result = ensureMonitorTaskFile({
|
|
549
|
+
cwd: dir,
|
|
550
|
+
env: VSCODE_ENV,
|
|
551
|
+
throughlineBin: FAKE_BIN,
|
|
552
|
+
});
|
|
553
|
+
assert.equal(result.action, 'repaired');
|
|
554
|
+
|
|
555
|
+
const obj = JSON.parse(readFileSync(join(dir, '.vscode', 'tasks.json'), 'utf8'));
|
|
556
|
+
assert.equal(obj.tasks.length, 3);
|
|
557
|
+
assert.equal(obj.tasks[0].label, 'Build');
|
|
558
|
+
assert.equal(obj.tasks[1].label, 'Throughline Monitor');
|
|
559
|
+
assert.equal(obj.tasks[1].command, process.execPath);
|
|
560
|
+
assert.equal(obj.tasks[2].label, 'Test');
|
|
561
|
+
} finally {
|
|
562
|
+
cleanup();
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
test('ensureMonitorTaskFile: already_present (not repaired) when task points to existing paths', () => {
|
|
567
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
568
|
+
try {
|
|
569
|
+
mkdirSync(join(dir, '.vscode'));
|
|
570
|
+
// command が現環境に存在するなら修復しない (process.execPath は必ず存在する)
|
|
571
|
+
const valid = {
|
|
572
|
+
version: '2.0.0',
|
|
573
|
+
tasks: [
|
|
574
|
+
{
|
|
575
|
+
label: 'Throughline Monitor',
|
|
576
|
+
type: 'shell',
|
|
577
|
+
command: process.execPath,
|
|
578
|
+
args: ['monitor'],
|
|
579
|
+
},
|
|
580
|
+
],
|
|
581
|
+
};
|
|
582
|
+
const tasksPath = join(dir, '.vscode', 'tasks.json');
|
|
583
|
+
writeFileSync(tasksPath, JSON.stringify(valid, null, 2));
|
|
584
|
+
const beforeMtime = statSync(tasksPath).mtimeMs;
|
|
585
|
+
|
|
586
|
+
const result = ensureMonitorTaskFile({
|
|
587
|
+
cwd: dir,
|
|
588
|
+
env: VSCODE_ENV,
|
|
589
|
+
throughlineBin: FAKE_BIN,
|
|
590
|
+
});
|
|
591
|
+
assert.equal(result.action, 'already_present');
|
|
592
|
+
|
|
593
|
+
const afterMtime = statSync(tasksPath).mtimeMs;
|
|
594
|
+
assert.equal(beforeMtime, afterMtime);
|
|
595
|
+
} finally {
|
|
596
|
+
cleanup();
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
test('ensureMonitorTaskFile: repaired emits notice on stdout', () => {
|
|
601
|
+
const { dir, cleanup } = mkTmpCwd();
|
|
602
|
+
const captured = [];
|
|
603
|
+
const origWrite = process.stdout.write.bind(process.stdout);
|
|
604
|
+
process.stdout.write = (chunk) => {
|
|
605
|
+
captured.push(typeof chunk === 'string' ? chunk : chunk.toString('utf8'));
|
|
606
|
+
return true;
|
|
607
|
+
};
|
|
608
|
+
try {
|
|
609
|
+
mkdirSync(join(dir, '.vscode'));
|
|
610
|
+
const stale = {
|
|
611
|
+
version: '2.0.0',
|
|
612
|
+
tasks: [
|
|
613
|
+
{
|
|
614
|
+
label: 'Throughline Monitor',
|
|
615
|
+
command: '/old/env/node',
|
|
616
|
+
args: ['/old/env/throughline.mjs', 'monitor'],
|
|
617
|
+
},
|
|
618
|
+
],
|
|
619
|
+
};
|
|
620
|
+
writeFileSync(join(dir, '.vscode', 'tasks.json'), JSON.stringify(stale, null, 2));
|
|
621
|
+
|
|
622
|
+
const result = ensureMonitorTaskFile({
|
|
623
|
+
cwd: dir,
|
|
624
|
+
env: VSCODE_ENV,
|
|
625
|
+
throughlineBin: FAKE_BIN,
|
|
626
|
+
});
|
|
627
|
+
assert.equal(result.action, 'repaired');
|
|
628
|
+
} finally {
|
|
629
|
+
process.stdout.write = origWrite;
|
|
630
|
+
cleanup();
|
|
631
|
+
}
|
|
632
|
+
const joined = captured.join('');
|
|
633
|
+
assert.ok(joined.includes('<system-reminder>'), 'repaired should emit notice');
|
|
634
|
+
assert.ok(joined.includes('自動修復'));
|
|
635
|
+
assert.ok(joined.includes('Reload Window'));
|
|
636
|
+
});
|
|
637
|
+
|
|
414
638
|
// --- ensureMonitorTaskFile: JSONC ---
|
|
415
639
|
|
|
416
640
|
test('ensureMonitorTaskFile: jsonc_unsupported for file with line comments', () => {
|
|
@@ -536,6 +760,13 @@ test('buildSetupNotice: returns notice text for merged', () => {
|
|
|
536
760
|
assert.ok(text.includes('Reload Window'));
|
|
537
761
|
});
|
|
538
762
|
|
|
763
|
+
test('buildSetupNotice: returns notice text for repaired', () => {
|
|
764
|
+
const text = buildSetupNotice('repaired');
|
|
765
|
+
assert.ok(text && text.includes('<system-reminder>'));
|
|
766
|
+
assert.ok(text.includes('自動修復'));
|
|
767
|
+
assert.ok(text.includes('Reload Window'));
|
|
768
|
+
});
|
|
769
|
+
|
|
539
770
|
test('buildSetupNotice: returns null for already_present (silent idempotency)', () => {
|
|
540
771
|
assert.equal(buildSetupNotice('already_present'), null);
|
|
541
772
|
});
|