leerness 1.36.298 → 1.36.300
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/CHANGELOG.md +52 -0
- package/README.md +4 -4
- package/bin/leerness.js +26 -12
- package/lib/copy-tree.js +5 -1
- package/lib/runtime-writes.js +24 -4
- package/package.json +9 -5
- package/scripts/bugfix-areas-probe.js +1 -1
- package/scripts/copy-tree-probe.js +17 -0
- package/scripts/done-status-probe.js +1 -1
- package/scripts/e2e-concurrency.js +24 -10
- package/scripts/e2e-lived-in-harness.js +2 -2
- package/scripts/fix-evidence-language-probe.js +1 -1
- package/scripts/i18n-next-cluster-probe.js +1 -1
- package/scripts/lazy-auto-track-probe.js +87 -0
- package/scripts/lock-probe.js +117 -10
- package/scripts/memory-archive-check-probe.js +1 -1
- package/scripts/memory-archive-retention-probe.js +2 -2
- package/scripts/memory-arena-cleanup-probe.js +106 -0
- package/scripts/memory-checked-input-probe.js +1 -1
- package/scripts/memory-guard-order-probe.js +1 -1
- package/scripts/memory-input-preflight-probe.js +1 -1
- package/scripts/memory-legacy-retention-probe.js +2 -2
- package/scripts/memory-migration-test-support.js +40 -2
- package/scripts/memory-output-publication-probe.js +1 -1
- package/scripts/memory-policy-root-spelling-probe.js +16 -6
- package/scripts/memory-projection-retention-probe.js +1 -1
- package/scripts/memory-recovery-external-probe.js +68 -1
- package/scripts/memory-recovery-receipt-probe.js +1 -1
- package/scripts/memory-restore-canonical-probe.js +1 -1
- package/scripts/memory-utf8-admission-probe.js +1 -1
- package/scripts/path-setup-powershell-probe.js +15 -0
- package/scripts/race-overlap-probe.js +148 -0
- package/scripts/race-overlap.js +28 -0
- package/scripts/release-bump-probe.js +1 -1
- package/scripts/review-request-labels-probe.js +1 -1
- package/scripts/review-request-signals-probe.js +1 -1
- package/scripts/skill-archive-probe.js +71 -0
- package/scripts/store-values-probe.js +9 -4
- package/scripts/task-sync-roundtrip-probe.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.300 - 2026-10-13
|
|
4
|
+
|
|
5
|
+
- Fixed: `leerness lazy detect --auto-track` registered TODOs that a task already covered (T-0345).
|
|
6
|
+
- When a batch of new TODOs mixed files that no tracker row references with a file a task already referenced, every new TODO got a task, including those in the referenced file.
|
|
7
|
+
- Now only the TODOs whose file no row references are registered, decided under the tracker lock as before, and `known-todos.json` gains exactly those. A TODO in a referenced file is left as it is, as without `--auto-track`.
|
|
8
|
+
- Tests (T-0345, T-0346):
|
|
9
|
+
- `scripts/lazy-auto-track-probe.js` (`npm run test:lazy-auto-track`, in `test:commands`) checks a mixed batch, a rerun, and two controls: nothing referenced, and everything referenced. On the old code the two mixed-batch cases fail: the referenced file's two TODOs got tasks too.
|
|
10
|
+
- The 30 races of `scripts/e2e-concurrency.js` now check that the two commands overlapped. Before, the first command's publication stalled for a fixed 1.2 s, and a second command that started later than that passed without racing.
|
|
11
|
+
- The second command now runs with `scripts/lock-probe.js` too. After each operation on the stalled file it reports: a finished read, open, stat or existence check, and the result of its first attempt at that file's lock or at a lock the first command held when it stalled (refused because held, or taken). It reads those held locks once, when it starts; if they cannot be read, the race does not count. It marks its report complete when it exits. A report that cannot be written sets its exit code to 97 at once, and an incomplete report does not count.
|
|
12
|
+
- The first command stays stalled until a report exists (at most 15 s), then 1.2 s more. `scripts/race-overlap.js` judges the reports. A race counts when such a lock attempt was refused because the first command held the lock. For a command that tried none of those locks, it counts when its read finished before the first command resumed. A first attempt that took such a lock means the first command had already released it, and the race does not count.
|
|
13
|
+
- `scripts/race-overlap-probe.js` (`npm run test:race-overlap`, in `test`, `test:core` and `test:fast`) runs the handshake with two small processes on ten fixed schedules. They include a second process that is slow to reach its lock attempt, one that reads during the stall but takes a lock after the release, held locks that cannot be read, and a report that can no longer be rewritten, also after the report was completed. Write failures are injected (`LOCKPROBE_REPORT_FAIL_AFTER`), so the test does not depend on file permissions. Ablations:
|
|
14
|
+
- every lock attempt reported as refused, whatever its result: 8 of 10;
|
|
15
|
+
- a first process that does not wait for the report: 6 of 10;
|
|
16
|
+
- the held locks read at the first lock attempt, and a failed read kept as "none": 8 of 10;
|
|
17
|
+
- a report judged without the completion mark: 9 of 10;
|
|
18
|
+
- a failed write that sets the exit code only in the closing exit listener: 9 of 10.
|
|
19
|
+
- Measured from the stall to the first report: 468-1,704 ms on this Windows PC and 123-217 ms on Linux (Docker node:20). In `creds-refresh` the second command reached the stalled file 1,704 ms after the stall began, 504 ms after the old fixed stall would have ended, so with the old harness that race would have passed without overlapping.
|
|
20
|
+
- On the current code all 30 races overlapped and passed, on Windows and on Linux. On Linux the second command's lock attempt was refused by the first command's lock in all 30; on Windows in 29, and `bugfix-start-vs-task-add` counted by its read. On Linux with the 1.36.296 code (before T-0343) all 30 overlapped too, and 20 failed: the 19 races T-0343 added and `plan-init-vs-remove`, whose `plan init` T-0343 also fixed. The other 10 pass there, as before.
|
|
21
|
+
|
|
22
|
+
## 1.36.299 - 2026-10-12
|
|
23
|
+
|
|
24
|
+
- Fixed: on Windows, `leerness skill publish` made a `.zip` instead of the `.tgz` when Git for Windows' GNU tar came first on PATH, as it does in Git Bash and in agent shells built on it (T-0352).
|
|
25
|
+
- Cause: the archive step gave `tar -czf` the absolute output path, and GNU tar reads the drive letter `C:` as a remote host ("Cannot connect to C:"). The step failed, and the Compress-Archive fallback made a `.zip`.
|
|
26
|
+
- The archive is now named relative to the `.leerness` folder, with forward slashes (`skills-publish-tarball/leerness-skills-<version>.tgz`). Windows' own tar (bsdtar) accepts that form too.
|
|
27
|
+
- Fixed: on Windows, `leerness path-setup --apply` could take a different folder for the npm bin folder and not add it (T-0353).
|
|
28
|
+
- Cause: it compared Path entries with PowerShell's `-contains`. That comparison uses the culture and ignores characters such as a soft hyphen (U+00AD), so an entry that differed from the bin folder only by such a character counted as present. Measured in Windows PowerShell 5.1 and PowerShell 7.
|
|
29
|
+
- Entries are now compared by ordinal value, letter case aside.
|
|
30
|
+
- Fixed: the recovery bundle that a memory write saves to the temp folder after a runtime-layout failure could be written inside the project (T-0349). The check that keeps it out compared the project and the temp folder as spelled.
|
|
31
|
+
- When the temp folder was named by an 8.3 short name (GitHub's Windows runners use `C:\Users\RUNNER~1\...`) or reached through a link, it could lie inside the project while its spelling did not.
|
|
32
|
+
- The check also took a folder inside the project whose name starts with two dots, such as `..temp`, for a folder outside it.
|
|
33
|
+
- The check now also compares their real paths, and only a first path part of exactly `..` counts as outside. A temp folder that does not resolve is refused, and so is a project path that fails to resolve for any reason other than being gone. A project path that no longer exists, because the project was renamed or removed during the operation, is compared as spelled only, as before, so its recovery is still saved.
|
|
34
|
+
- The bundle is now written below the temp folder's real path, the one that was checked, and the recovery folder it reports is named by that path. A temp link retargeted into the project after the check can no longer carry the bundle there.
|
|
35
|
+
- Tests (T-0347, T-0349, T-0352, T-0353, T-0354, T-0355):
|
|
36
|
+
- `scripts/skill-archive-probe.js` (`npm run test:skill-archive`, in `test`, `test:core` and `test:fast`) asks the archive step for the `.tgz` with each tar the machine has first on PATH, and checks that tar made it, with the expected entries. Here that is Windows' bsdtar and Git for Windows' GNU tar. With the old absolute path it passes 2 of 6.
|
|
37
|
+
- `scripts/memory-recovery-external-probe.js` (21 tests) adds nine. The old check fails eight of them:
|
|
38
|
+
- A temp folder inside the project, named through a link outside the project or by its 8.3 short name, must be refused before any recovery work. So must a folder inside the project whose name starts with two dots, named directly or through a link.
|
|
39
|
+
- A temp folder that does not resolve must be refused, also when the project path is gone. So must a project path that fails to resolve with an error other than "not found" (simulated). With the old check, an unresolvable temp folder failed the recovery later, with ENOENT.
|
|
40
|
+
- A temp link retargeted into the project while the check runs (simulated) must not carry the bundle into the project.
|
|
41
|
+
- A project renamed away during the operation must still get its recovery outside it. The old check passes this one.
|
|
42
|
+
- `scripts/path-setup-powershell-probe.js` (19 checks):
|
|
43
|
+
- An entry that differs from the bin folder by a soft hyphen must not count as present. The old script fails it.
|
|
44
|
+
- Every registry run now requires the parser check first. With a command injected into the script, no registry run happened. With that requirement removed, the injected command ran.
|
|
45
|
+
- `scripts/memory-policy-root-spelling-probe.js`: the MCP checks and the resolve check use the first non-canonical spelling the machine has. Before, they always used the lowercase drive letter, which a UNC path does not have. They print SKIP when there is none.
|
|
46
|
+
- The shared support of the memory probes (`scripts/memory-migration-test-support.js`) now removes its temp arena when every test passed, and keeps it when one failed. Links inside the arena are removed as links first, so their targets are left alone. An arena, or a folder in it, that was replaced with a link is neither walked nor removed. This holds also when the replacement happens while `finish()` runs: each folder must still resolve inside the arena just before it is listed and before a link in it is removed. A process that kept replacing folders during `finish()` could still race those checks; `finish()` runs after the tests.
|
|
47
|
+
- Before, every run left one arena (1,206 were in this machine's temp folder on 2026-10-11).
|
|
48
|
+
- `scripts/memory-arena-cleanup-probe.js` (`npm run test:memory-arena-cleanup`, in `test`, `test:core` and `test:fast`) checks five cases with links to folders outside the arena, three of them with a replacement before or during `finish()`. On the old support all five fail: the removal case because nothing is removed, the failing-run and replace-before cases only on the note that `finish()` now prints, and the two replacement-during cases because the old `finish()` never looks at the arena, so the replacement never happens.
|
|
49
|
+
- The 24 `fs.cpSync` copies in 21 test files now use `lib/copy-tree.js` (T-0347). On Windows with Node 22.23.3, `fs.cpSync` ends the process with an access violation when a path has non-ASCII letters, as the temp folder does for a Windows user name in Korean.
|
|
50
|
+
- Measured with the temp folder set to `...\AppData\Local\Temp\임시한글` on Node 22.23.3: on the old code all 21 files crashed (Segmentation fault, exit 139 in Git Bash). With the change, 20 passed.
|
|
51
|
+
- The 21st, `scripts/store-values-probe.js`, failed one language check, which read the Hangul in the temp path as Korean text. Its language checks now mask the test folder's path first, and it passes.
|
|
52
|
+
- On Windows, `lib/copy-tree.js` now resolves a relative directory link against the copy's own folder, so the copied link names its sibling in the copy. Before, Node resolved the junction target against the process's working folder. On POSIX the relative link is copied as written and names the same sibling. `fs.cpSync` instead points such a link back into the source tree.
|
|
53
|
+
- `scripts/copy-tree-probe.js` checks this where links can be made. A Windows machine without the symlink privilege skips it.
|
|
54
|
+
|
|
3
55
|
## 1.36.298 - 2026-10-11
|
|
4
56
|
|
|
5
57
|
- Fixed: on Windows, `leerness path-setup --apply` never registered the npm bin folder in the user PATH (T-0351). This was so from 1.9.254.
|
package/README.md
CHANGED
|
@@ -195,7 +195,7 @@ MIT
|
|
|
195
195
|
<!-- leerness:project-readme:start -->
|
|
196
196
|
## Leerness Project Harness
|
|
197
197
|
|
|
198
|
-
이 프로젝트는 Leerness v1.36.
|
|
198
|
+
이 프로젝트는 Leerness v1.36.300 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
199
199
|
|
|
200
200
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
201
201
|
|
|
@@ -249,7 +249,7 @@ leerness memory restore decision <date|title>
|
|
|
249
249
|
|
|
250
250
|
### MCP server (외부 AI 통합)
|
|
251
251
|
|
|
252
|
-
Leerness v1.36.
|
|
252
|
+
Leerness v1.36.300는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **102개 도구**를 노출:
|
|
253
253
|
|
|
254
254
|
```jsonc
|
|
255
255
|
// 카테고리별
|
|
@@ -270,7 +270,7 @@ Leerness v1.36.298는 stdio JSON-RPC MCP server를 내장합니다 — Claude Co
|
|
|
270
270
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
271
271
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) 회귀 테스트 갱신 → 4) 전체 e2e 스위트 통과 → 5) npm publish + git tag → 6) main push → 7) session close → 8) 다음 라운드 예약.
|
|
272
272
|
|
|
273
|
-
현재 사용 중인 하네스 버전: **1.36.
|
|
273
|
+
현재 사용 중인 하네스 버전: **1.36.300**. 공개 릴리스 여부는 공개 저장소의 태그와 배포 기록에서 별도로 확인합니다.
|
|
274
274
|
|
|
275
275
|
### 성능 가이드
|
|
276
276
|
|
|
@@ -308,7 +308,7 @@ leerness release pack --close --auto-main-push
|
|
|
308
308
|
- `.leerness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
309
309
|
- `.leerness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
310
310
|
|
|
311
|
-
Last synced by Leerness v1.36.
|
|
311
|
+
Last synced by Leerness v1.36.300: 2026-09-30
|
|
312
312
|
<!-- leerness:project-readme:end -->
|
|
313
313
|
GitHub 로그인 계정 확인용 서버 어댑터: [설정·연결·승인과의 구분](docs/github-login.md). 실제 앱 등록/브라우저 세션 연결과 대상별 승인은 후속입니다.
|
|
314
314
|
|
package/bin/leerness.js
CHANGED
|
@@ -115,7 +115,7 @@ const {
|
|
|
115
115
|
migrateLegacyWorkspace,
|
|
116
116
|
} = require('../lib/workspace-dir');
|
|
117
117
|
|
|
118
|
-
const VERSION = '1.36.
|
|
118
|
+
const VERSION = '1.36.300';
|
|
119
119
|
|
|
120
120
|
// MCP lifecycle 주소 표식은 현재 CLI 호출 한 번에만 유효하다. CLI bootstrap에서 즉시 env에서
|
|
121
121
|
// 떼어 두어 `--no-record`/hook처럼 presence 기록 함수에 도달하지 않는 경로도 후속 child에 유출하지 않는다.
|
|
@@ -3998,7 +3998,7 @@ function _pathDiagnose() {
|
|
|
3998
3998
|
// do to the user Path (review round 1): the read expands %VAR% entries with this process's values, and the write
|
|
3999
3999
|
// stores REG_SZ, so a REG_EXPAND_SZ Path (the Windows default, e.g. %USERPROFILE%\...\WindowsApps) would lose its
|
|
4000
4000
|
// references. The script reads the raw value and its registry type and writes both back. An entry counts as present
|
|
4001
|
-
// only as written (letter case aside): expanding %VAR% here would use this shell's variables, which new terminals may
|
|
4001
|
+
// only as written (ordinal, letter case aside): expanding %VAR% here would use this shell's variables, which new terminals may
|
|
4002
4002
|
// not have, and could report EXISTS for a directory they cannot reach (review round 3; the owner chose this). The
|
|
4003
4003
|
// cost is a harmless second entry when the Path already names the directory through a variable. Deleting a variable
|
|
4004
4004
|
// that does not exist through SetEnvironmentVariable(..., 'User') writes nothing but sends the WM_SETTINGCHANGE that
|
|
@@ -4020,7 +4020,10 @@ function _winPathPsScript() {
|
|
|
4020
4020
|
+ "$kind = [Microsoft.Win32.RegistryValueKind]::ExpandString; "
|
|
4021
4021
|
+ "if ($names -contains 'Path') { $kind = $k.GetValueKind('Path') }; "
|
|
4022
4022
|
+ "if ($kind -ne [Microsoft.Win32.RegistryValueKind]::String -and $kind -ne [Microsoft.Win32.RegistryValueKind]::ExpandString) { throw ('the user Path has registry type ' + $kind) }; "
|
|
4023
|
-
|
|
4023
|
+
// T-0353: ordinal, case-insensitive. -contains compares with the culture, which ignores characters such as a
|
|
4024
|
+
// soft hyphen (U+00AD), so 'C:\np<U+00AD>m' counted as 'C:\npm' (measured in Windows PowerShell 5.1 and 7).
|
|
4025
|
+
+ "$found = $false; foreach ($p in $raw.Split(';')) { if ([string]::Equals($p, $b, [StringComparison]::OrdinalIgnoreCase)) { $found = $true } }; "
|
|
4026
|
+
+ "if ($found) { [Console]::Out.WriteLine('EXISTS') } "
|
|
4024
4027
|
+ "else { $nu = if ($raw -eq '') { $b } else { $raw.TrimEnd(';') + ';' + $b }; "
|
|
4025
4028
|
+ "$k.SetValue('Path', $nu, $kind); "
|
|
4026
4029
|
+ "[Environment]::SetEnvironmentVariable('LEERNESS_PATH_SETUP_NOTIFY', $null, 'User'); "
|
|
@@ -17525,23 +17528,27 @@ function lazyDetect(root, opts = {}) {
|
|
|
17525
17528
|
const header = progressHeader(root, input.text);
|
|
17526
17529
|
const rows2 = readProgressRows(root, input.text);
|
|
17527
17530
|
const lockedText = rows2.map(r => `${r.request || ''} ${r.evidence || ''}`).join('\n');
|
|
17528
|
-
|
|
17531
|
+
// T-0345: only the TODOs whose file no row references. A batch with one untracked file registered every
|
|
17532
|
+
// new TODO, so TODOs in files a task already covered were registered again.
|
|
17533
|
+
const toRegister = newTodos.filter(t => !lockedText.includes(t.file));
|
|
17534
|
+
if (!toRegister.length) return [];
|
|
17529
17535
|
let maxT = _historyRetention.reservedTaskMax(root); const idRe = /\bT-(\d{4,})\b/g;
|
|
17530
17536
|
const scanSrc = (exists(planPath(root)) ? read(planPath(root)) : '') + '\n' + rows2.map(r => r.id).join('\n');
|
|
17531
17537
|
let mm; while ((mm = idRe.exec(scanSrc))) maxT = Math.max(maxT, Number(mm[1]));
|
|
17532
|
-
_historyRetention.assertTaskIdCapacity(maxT,
|
|
17533
|
-
for (const t of
|
|
17538
|
+
_historyRetention.assertTaskIdCapacity(maxT, toRegister.length);
|
|
17539
|
+
for (const t of toRegister) {
|
|
17534
17540
|
maxT++;
|
|
17535
17541
|
rows2.push({ id: `T-${String(maxT).padStart(4, '0')}`, status: 'requested', request: `TODO ${t.file}:${t.line}`, evidence: 'auto-tracked', nextAction: t.text.slice(0, 80), updated: today() });
|
|
17536
17542
|
}
|
|
17537
17543
|
writeProgressRows(root, header, rows2, { input });
|
|
17538
|
-
return
|
|
17544
|
+
return toRegister;
|
|
17539
17545
|
});
|
|
17540
|
-
if (registered) {
|
|
17541
|
-
// known-todos에 추가 — 다음 detect에서 재카운트 안
|
|
17542
|
-
|
|
17546
|
+
if (registered.length) {
|
|
17547
|
+
// known-todos에 추가 — 다음 detect에서 재카운트 안 하도록. T-0345: only what was registered; a TODO in a
|
|
17548
|
+
// file a task already references stays as it is, as without --auto-track.
|
|
17549
|
+
const merged = [...knownList, ...registered.map(t => ({ ...t, ackAt: now() }))];
|
|
17543
17550
|
writeUtf8(knownPath, JSON.stringify(merged, null, 2) + '\n');
|
|
17544
|
-
if (!jsonMode) ok(`${
|
|
17551
|
+
if (!jsonMode) ok(`${registered.length}개 TODO를 progress-tracker에 자동 등록 + known-todos.json 갱신`);
|
|
17545
17552
|
}
|
|
17546
17553
|
} else if (newTodos.length && !jsonMode) {
|
|
17547
17554
|
log(` 💡 자동 등록: leerness lazy detect --auto-track`);
|
|
@@ -26493,7 +26500,12 @@ function _createArchive(cwd, sourceDir, outPath) {
|
|
|
26493
26500
|
// 1) tar.gz (POSIX 환경에서 가장 안정)
|
|
26494
26501
|
if (/\.(tgz|tar\.gz)$/i.test(outPath)) {
|
|
26495
26502
|
tried.push('tar');
|
|
26496
|
-
|
|
26503
|
+
// T-0352: the archive is named relative to cwd, with forward slashes. Git for Windows' GNU tar, first on PATH in
|
|
26504
|
+
// Git Bash, reads the drive letter of an absolute Windows path as a remote host ("Cannot connect to C:") and
|
|
26505
|
+
// failed, so the .zip fallback ran instead. bsdtar accepts either form. A path on another drive stays absolute.
|
|
26506
|
+
const relativeOut = path.relative(cwd, outPath);
|
|
26507
|
+
const tarOut = relativeOut && !path.isAbsolute(relativeOut) ? relativeOut.split(path.sep).join('/') : outPath;
|
|
26508
|
+
const r = _spawnPortableArgvSync('tar', ['-czf', tarOut, sourceDir], {
|
|
26497
26509
|
encoding: 'utf8', timeout: 30000, shell: false, cwd
|
|
26498
26510
|
});
|
|
26499
26511
|
if (r.status === 0 && exists(outPath)) return { ok: true, path: outPath, method: 'tar', tried };
|
|
@@ -34146,6 +34158,8 @@ module.exports = {
|
|
|
34146
34158
|
_cliMutationClass, _mustRemainReadOnly, _runtimeProjectRoot,
|
|
34147
34159
|
_npmGlobalBin, _dirInPath, _leernessResolvable, _pathDiagnose, _registerPath,
|
|
34148
34160
|
_winPathPsScript, _unixPathBlock, pathSetupCmd,
|
|
34161
|
+
// T-0352: the skill publish archive chain (tar, then the zip fallbacks), for scripts/skill-archive-probe.js.
|
|
34162
|
+
_createArchive,
|
|
34149
34163
|
_isSecretKey, compareVer, parseHarnessVersion,
|
|
34150
34164
|
// 1.9.257: CJK 인코딩 분류 (UR-0014 계열) 순수 함수 — 단위 테스트
|
|
34151
34165
|
_classifyCJK, _riskLabel,
|
package/lib/copy-tree.js
CHANGED
|
@@ -12,10 +12,14 @@ const path = require('path');
|
|
|
12
12
|
function copyTreeSync(src, dst) {
|
|
13
13
|
const stat = fs.lstatSync(src);
|
|
14
14
|
if (stat.isSymbolicLink()) {
|
|
15
|
-
|
|
15
|
+
let target = fs.readlinkSync(src);
|
|
16
16
|
let type;
|
|
17
17
|
if (process.platform === 'win32') {
|
|
18
18
|
try { type = fs.statSync(src).isDirectory() ? 'junction' : 'file'; } catch { type = 'file'; }
|
|
19
|
+
// T-0347: a junction's target must be absolute, and Node resolves a relative one against the process cwd.
|
|
20
|
+
// A relative directory link is resolved against the copy's own folder, so it still names the place it named
|
|
21
|
+
// relative to where it now is (fs.cpSync instead points it back into the source tree).
|
|
22
|
+
if (type === 'junction' && !path.isAbsolute(target)) target = path.resolve(path.dirname(dst), target);
|
|
19
23
|
}
|
|
20
24
|
fs.symlinkSync(target, dst, type);
|
|
21
25
|
return;
|
package/lib/runtime-writes.js
CHANGED
|
@@ -159,13 +159,33 @@ function assertExternalRecoveryAllowed(root) {
|
|
|
159
159
|
}
|
|
160
160
|
const project = path.resolve(root);
|
|
161
161
|
const base = path.resolve(os.tmpdir());
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
// T-0349: compared as spelled and as real paths. A temp folder reached through a link, or named by an 8.3 short
|
|
163
|
+
// name (GitHub's Windows runners give C:\Users\RUNNER~1\...), can lie inside the project while its spelling does
|
|
164
|
+
// not, and the recovery bundle was then written into the project it must stay out of. A temp folder that cannot
|
|
165
|
+
// be resolved counts as unsafe. A project path that no longer exists (ENOENT or ENOTDIR: renamed or removed during
|
|
166
|
+
// the operation, one way this latch is reached) has nothing at it to lie inside, so only its spelling is compared;
|
|
167
|
+
// any other failure to resolve it counts as unsafe.
|
|
168
|
+
const unsafe = () => {
|
|
164
169
|
const error = new Error(`External recovery temp must stay outside project root: ${base}`);
|
|
165
170
|
error.code = 'E_RECOVERY_TEMP_UNSAFE';
|
|
166
171
|
throw error;
|
|
167
|
-
}
|
|
168
|
-
|
|
172
|
+
};
|
|
173
|
+
// Outside means a first component of exactly '..' (a child folder may be named '..temp', review round 1).
|
|
174
|
+
const inside = (from, to) => {
|
|
175
|
+
const relative = path.relative(from, to);
|
|
176
|
+
return !(relative === '..' || relative.startsWith('..' + path.sep) || path.isAbsolute(relative));
|
|
177
|
+
};
|
|
178
|
+
const real = p => { try { return { path: fs.realpathSync.native(p) }; } catch (error) { return { error }; } };
|
|
179
|
+
const realBase = real(base), realProject = real(project);
|
|
180
|
+
if (!realBase.path) unsafe();
|
|
181
|
+
const missing = !realProject.path && ['ENOENT', 'ENOTDIR'].includes(realProject.error && realProject.error.code);
|
|
182
|
+
if (!realProject.path && !missing) unsafe();
|
|
183
|
+
if (inside(project, base)) unsafe();
|
|
184
|
+
if (realProject.path && inside(realProject.path, realBase.path)) unsafe();
|
|
185
|
+
// The bundle goes below the real path that was checked, not below the alias: a temp link retargeted into the project
|
|
186
|
+
// after this check would otherwise carry the bundle there (review round 2). A real folder replaced while the
|
|
187
|
+
// recovery runs is not covered, as for every path-based write here.
|
|
188
|
+
return realBase.path;
|
|
169
189
|
}
|
|
170
190
|
|
|
171
191
|
// Streaming recovery uses the same latch and external-only destination. The
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.300",
|
|
4
4
|
"description": "The AI-coding operations layer that makes \"done\" require evidence — persistent memory, evidence-gated completion checks, and clean handoffs for any AI agent (Claude Code, Codex, Cursor). State lives as plain files in your repo. CLI + MCP, 0 runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"leerness",
|
|
@@ -48,15 +48,15 @@
|
|
|
48
48
|
"LICENSE"
|
|
49
49
|
],
|
|
50
50
|
"scripts": {
|
|
51
|
-
"test": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && npm run test:state-scopes && npm run test:runtime-compatibility && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/e2e-repo-guard.js && node ./scripts/e2e-lived-in-harness.js && npm run test:lock-heartbeat && npm run test:temp-cleanup && npm run test:path-setup && node ./scripts/platform-smoke.js && node ./scripts/parent-detect-selftest-probe.js && node ./scripts/verify-code-cross-runtime-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./scripts/i18n-priority-surface-probe.js && npm run test:harness-text && npm run test:test-plan && node ./scripts/i18n-next-cluster-probe.js && node ./scripts/handoff-i18n-probe.js && node ./scripts/provider-capacity-probe.js && npm run test:provider-response && npm run test:github-login && npm run test:provider-dispatch && npm run test:provider-receipt && npm run test:provider-observation && npm run test:ui-review-publication && npm run test:ui-review-target && npm run test:ui-review-authorization && npm run test:ui-review-authority-host && npm run test:ui-review-authority-floor && npm run test:ui-review-proof-use && npm run test:ui-review-recovery && node ./scripts/file-lease-probe.js && node ./scripts/role-agent-schema-probe.js && node ./scripts/role-store-loader-probe.js && node ./scripts/role-fallback-probe.js && npm run test:agent-bench-output && npm run test:agent-chat-stream && npm run test:encoding-diagnostics && npm run test:managed-text && npm run test:plan-text && npm run test:session-close-text && npm run test:brief-text && npm run test:generation-text && npm run test:install-text && npm run test:authoring-language && npm run test:context-reading && npm run test:rule-text && npm run test:mode-text && npm run test:handoff-text && npm run test:audit-text && npm run test:audit-readme-text && npm run test:audit-env-text && npm run test:feature-graph-text && npm run test:env-sync-text && npm run test:teams-text && npm run test:reminder-text && npm run test:handoff-history-text && npm run test:env-snapshot-text && npm run test:context-current && npm run test:review-feedback && npm run test:ledger-sync && npm run test:state-lock && npm run test:history-retention && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/handoff-readonly-probe.js && node ./scripts/mcp-presence-probe.js && npm run test:commands && npm run test:installed && node ./scripts/e2e.js && node ./scripts/review-checkpoint-probe.js && node ./scripts/review-checkpoint-transport-probe.js && node ./scripts/review-snapshot-probe.js && npm run test:memory-migration && npm run test:memory-items && npm run test:memory-budget && npm run test:memory-footprint && npm run test:memory-write-budget && npm run test:memory-write-preparation && npm run test:memory-write-admission && npm run test:memory-command-policy && npm run test:memory-publication-policy && npm run test:memory-recovery-transfer && npm run test:ui-boundary && node ./scripts/review-control-probe.js && npm run test:ui-review-run && npm run test:rule-verification",
|
|
52
|
-
"test:core": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && npm run test:state-scopes && npm run test:runtime-compatibility && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/e2e-repo-guard.js && node ./scripts/e2e-lived-in-harness.js && npm run test:lock-heartbeat && npm run test:temp-cleanup && npm run test:path-setup && node ./scripts/platform-smoke.js && node ./scripts/parent-detect-selftest-probe.js && node ./scripts/verify-code-cross-runtime-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./scripts/i18n-priority-surface-probe.js && npm run test:harness-text && npm run test:test-plan && node ./scripts/i18n-next-cluster-probe.js && node ./scripts/handoff-i18n-probe.js && node ./scripts/provider-capacity-probe.js && npm run test:provider-response && npm run test:github-login && npm run test:provider-dispatch && npm run test:provider-receipt && npm run test:provider-observation && npm run test:ui-review-publication-smoke && npm run test:ui-review-target-smoke && npm run test:ui-review-authorization-smoke && npm run test:ui-review-authority-host-smoke && npm run test:ui-review-authority-floor-smoke && npm run test:ui-review-proof-use-smoke && npm run test:ui-review-recovery && node ./scripts/file-lease-probe.js && node ./scripts/role-agent-schema-probe.js && node ./scripts/role-store-loader-probe.js && node ./scripts/role-fallback-probe.js && npm run test:agent-bench-output && npm run test:review-feedback && npm run test:ledger-sync && npm run test:state-lock && npm run test:history-retention && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/memory-guard-order-probe.js && node ./scripts/memory-input-preflight-probe.js && node ./scripts/memory-utf8-admission-probe.js && node ./scripts/memory-checked-input-probe.js && node ./scripts/memory-input-unit-probe.js && node ./scripts/memory-restore-canonical-probe.js && node ./scripts/memory-archive-retention-probe.js && node ./scripts/memory-archive-check-probe.js && node ./scripts/memory-projection-retention-probe.js && node ./scripts/memory-language-probe.js && node ./scripts/memory-legacy-retention-probe.js && node ./scripts/memory-recovery-receipt-probe.js && node ./scripts/memory-output-unit-probe.js && node ./scripts/memory-output-posix-probe.js && node ./scripts/memory-output-publication-probe.js && node ./scripts/memory-output-parent-race-probe.js && node ./scripts/handoff-readonly-probe.js && node ./scripts/mcp-presence-probe.js && node ./scripts/rule-applicability-probe.js && node ./scripts/review-checkpoint-probe.js && node ./scripts/review-checkpoint-transport-probe.js && node ./scripts/review-snapshot-probe.js && node ./scripts/review-snapshot-transport-probe.js && npm run test:memory-migration && npm run test:memory-items && npm run test:memory-budget && npm run test:memory-footprint && npm run test:memory-write-budget && npm run test:memory-write-preparation && npm run test:memory-write-admission && npm run test:memory-command-policy && npm run test:memory-publication-policy && npm run test:memory-recovery-transfer && npm run test:ui-boundary && npm run test:ui-review-run && npm run test:rule-verification",
|
|
53
|
-
"test:commands": "npm run test:hooks && npm run test:e2e-observation && node ./scripts/memory-guard-order-probe.js && node ./scripts/memory-input-preflight-probe.js && node ./scripts/memory-utf8-admission-probe.js && node ./scripts/memory-checked-input-probe.js && node ./scripts/memory-input-unit-probe.js && node ./scripts/memory-restore-canonical-probe.js && node ./scripts/memory-archive-retention-probe.js && node ./scripts/memory-archive-check-probe.js && node ./scripts/memory-projection-retention-probe.js && node ./scripts/memory-language-probe.js && node ./scripts/memory-legacy-retention-probe.js && node ./scripts/memory-recovery-receipt-probe.js && node ./scripts/memory-output-unit-probe.js && node ./scripts/memory-output-posix-probe.js && node ./scripts/memory-output-publication-probe.js && node ./scripts/memory-output-parent-race-probe.js && node ./scripts/command-flags-probe.js && node ./scripts/dead-flags-probe.js && node ./scripts/free-text-args-probe.js && node ./scripts/title-args-probe.js && node ./scripts/false-claim-probe.js && node ./scripts/claim-sweep-git-probe.js && node ./scripts/claim-runtime-cache-probe.js && node ./scripts/runtime-cost-probe.js && node ./scripts/secret-scan-scope-probe.js && node ./scripts/handoff-preservation-probe.js && node ./scripts/next-action-suggestion-probe.js && node ./scripts/api-skill-match-probe.js && node ./scripts/crud-check-probe.js && node ./scripts/claims-baseline-probe.js && node ./scripts/claims-baseline-concurrency-probe.js && node ./scripts/e2e-command-surface.js && node ./scripts/rule-applicability-probe.js && node ./scripts/review-request-labels-probe.js && node ./scripts/review-request-signals-probe.js && node ./scripts/store-values-probe.js && node ./scripts/review-snapshot-transport-probe.js && node ./scripts/bugfix-areas-probe.js && node ./scripts/done-status-probe.js && node ./scripts/fix-evidence-language-probe.js && node ./scripts/decision-fence-offset-probe.js && node ./scripts/task-sync-roundtrip-probe.js && node ./scripts/tracker-write-language-probe.js && node ./scripts/write-path-language-probe.js && node ./scripts/write-primitive-language-probe.js && node ./scripts/dry-run-refusal-probe.js",
|
|
51
|
+
"test": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && npm run test:state-scopes && npm run test:runtime-compatibility && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/race-overlap-probe.js && node ./scripts/e2e-repo-guard.js && node ./scripts/e2e-lived-in-harness.js && npm run test:lock-heartbeat && npm run test:temp-cleanup && npm run test:path-setup && npm run test:skill-archive && npm run test:memory-arena-cleanup && node ./scripts/platform-smoke.js && node ./scripts/parent-detect-selftest-probe.js && node ./scripts/verify-code-cross-runtime-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./scripts/i18n-priority-surface-probe.js && npm run test:harness-text && npm run test:test-plan && node ./scripts/i18n-next-cluster-probe.js && node ./scripts/handoff-i18n-probe.js && node ./scripts/provider-capacity-probe.js && npm run test:provider-response && npm run test:github-login && npm run test:provider-dispatch && npm run test:provider-receipt && npm run test:provider-observation && npm run test:ui-review-publication && npm run test:ui-review-target && npm run test:ui-review-authorization && npm run test:ui-review-authority-host && npm run test:ui-review-authority-floor && npm run test:ui-review-proof-use && npm run test:ui-review-recovery && node ./scripts/file-lease-probe.js && node ./scripts/role-agent-schema-probe.js && node ./scripts/role-store-loader-probe.js && node ./scripts/role-fallback-probe.js && npm run test:agent-bench-output && npm run test:agent-chat-stream && npm run test:encoding-diagnostics && npm run test:managed-text && npm run test:plan-text && npm run test:session-close-text && npm run test:brief-text && npm run test:generation-text && npm run test:install-text && npm run test:authoring-language && npm run test:context-reading && npm run test:rule-text && npm run test:mode-text && npm run test:handoff-text && npm run test:audit-text && npm run test:audit-readme-text && npm run test:audit-env-text && npm run test:feature-graph-text && npm run test:env-sync-text && npm run test:teams-text && npm run test:reminder-text && npm run test:handoff-history-text && npm run test:env-snapshot-text && npm run test:context-current && npm run test:review-feedback && npm run test:ledger-sync && npm run test:state-lock && npm run test:history-retention && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/handoff-readonly-probe.js && node ./scripts/mcp-presence-probe.js && npm run test:commands && npm run test:installed && node ./scripts/e2e.js && node ./scripts/review-checkpoint-probe.js && node ./scripts/review-checkpoint-transport-probe.js && node ./scripts/review-snapshot-probe.js && npm run test:memory-migration && npm run test:memory-items && npm run test:memory-budget && npm run test:memory-footprint && npm run test:memory-write-budget && npm run test:memory-write-preparation && npm run test:memory-write-admission && npm run test:memory-command-policy && npm run test:memory-publication-policy && npm run test:memory-recovery-transfer && npm run test:ui-boundary && node ./scripts/review-control-probe.js && npm run test:ui-review-run && npm run test:rule-verification",
|
|
52
|
+
"test:core": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && npm run test:state-scopes && npm run test:runtime-compatibility && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/race-overlap-probe.js && node ./scripts/e2e-repo-guard.js && node ./scripts/e2e-lived-in-harness.js && npm run test:lock-heartbeat && npm run test:temp-cleanup && npm run test:path-setup && npm run test:skill-archive && npm run test:memory-arena-cleanup && node ./scripts/platform-smoke.js && node ./scripts/parent-detect-selftest-probe.js && node ./scripts/verify-code-cross-runtime-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./scripts/i18n-priority-surface-probe.js && npm run test:harness-text && npm run test:test-plan && node ./scripts/i18n-next-cluster-probe.js && node ./scripts/handoff-i18n-probe.js && node ./scripts/provider-capacity-probe.js && npm run test:provider-response && npm run test:github-login && npm run test:provider-dispatch && npm run test:provider-receipt && npm run test:provider-observation && npm run test:ui-review-publication-smoke && npm run test:ui-review-target-smoke && npm run test:ui-review-authorization-smoke && npm run test:ui-review-authority-host-smoke && npm run test:ui-review-authority-floor-smoke && npm run test:ui-review-proof-use-smoke && npm run test:ui-review-recovery && node ./scripts/file-lease-probe.js && node ./scripts/role-agent-schema-probe.js && node ./scripts/role-store-loader-probe.js && node ./scripts/role-fallback-probe.js && npm run test:agent-bench-output && npm run test:review-feedback && npm run test:ledger-sync && npm run test:state-lock && npm run test:history-retention && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/memory-guard-order-probe.js && node ./scripts/memory-input-preflight-probe.js && node ./scripts/memory-utf8-admission-probe.js && node ./scripts/memory-checked-input-probe.js && node ./scripts/memory-input-unit-probe.js && node ./scripts/memory-restore-canonical-probe.js && node ./scripts/memory-archive-retention-probe.js && node ./scripts/memory-archive-check-probe.js && node ./scripts/memory-projection-retention-probe.js && node ./scripts/memory-language-probe.js && node ./scripts/memory-legacy-retention-probe.js && node ./scripts/memory-recovery-receipt-probe.js && node ./scripts/memory-output-unit-probe.js && node ./scripts/memory-output-posix-probe.js && node ./scripts/memory-output-publication-probe.js && node ./scripts/memory-output-parent-race-probe.js && node ./scripts/handoff-readonly-probe.js && node ./scripts/mcp-presence-probe.js && node ./scripts/rule-applicability-probe.js && node ./scripts/review-checkpoint-probe.js && node ./scripts/review-checkpoint-transport-probe.js && node ./scripts/review-snapshot-probe.js && node ./scripts/review-snapshot-transport-probe.js && npm run test:memory-migration && npm run test:memory-items && npm run test:memory-budget && npm run test:memory-footprint && npm run test:memory-write-budget && npm run test:memory-write-preparation && npm run test:memory-write-admission && npm run test:memory-command-policy && npm run test:memory-publication-policy && npm run test:memory-recovery-transfer && npm run test:ui-boundary && npm run test:ui-review-run && npm run test:rule-verification",
|
|
53
|
+
"test:commands": "npm run test:hooks && npm run test:e2e-observation && node ./scripts/memory-guard-order-probe.js && node ./scripts/memory-input-preflight-probe.js && node ./scripts/memory-utf8-admission-probe.js && node ./scripts/memory-checked-input-probe.js && node ./scripts/memory-input-unit-probe.js && node ./scripts/memory-restore-canonical-probe.js && node ./scripts/memory-archive-retention-probe.js && node ./scripts/memory-archive-check-probe.js && node ./scripts/memory-projection-retention-probe.js && node ./scripts/memory-language-probe.js && node ./scripts/memory-legacy-retention-probe.js && node ./scripts/memory-recovery-receipt-probe.js && node ./scripts/memory-output-unit-probe.js && node ./scripts/memory-output-posix-probe.js && node ./scripts/memory-output-publication-probe.js && node ./scripts/memory-output-parent-race-probe.js && node ./scripts/command-flags-probe.js && node ./scripts/dead-flags-probe.js && node ./scripts/free-text-args-probe.js && node ./scripts/title-args-probe.js && node ./scripts/false-claim-probe.js && node ./scripts/claim-sweep-git-probe.js && node ./scripts/claim-runtime-cache-probe.js && node ./scripts/runtime-cost-probe.js && node ./scripts/secret-scan-scope-probe.js && node ./scripts/handoff-preservation-probe.js && node ./scripts/next-action-suggestion-probe.js && node ./scripts/api-skill-match-probe.js && node ./scripts/crud-check-probe.js && node ./scripts/claims-baseline-probe.js && node ./scripts/claims-baseline-concurrency-probe.js && node ./scripts/e2e-command-surface.js && node ./scripts/rule-applicability-probe.js && node ./scripts/review-request-labels-probe.js && node ./scripts/review-request-signals-probe.js && node ./scripts/store-values-probe.js && node ./scripts/review-snapshot-transport-probe.js && node ./scripts/bugfix-areas-probe.js && node ./scripts/done-status-probe.js && node ./scripts/fix-evidence-language-probe.js && node ./scripts/decision-fence-offset-probe.js && node ./scripts/task-sync-roundtrip-probe.js && node ./scripts/tracker-write-language-probe.js && node ./scripts/write-path-language-probe.js && node ./scripts/write-primitive-language-probe.js && node ./scripts/dry-run-refusal-probe.js && node ./scripts/lazy-auto-track-probe.js",
|
|
54
54
|
"test:e2e-observation": "node ./scripts/e2e-temp-scope-probe.js && node ./scripts/e2e-child-diagnostics-probe.js",
|
|
55
55
|
"test:next-actions": "node ./scripts/next-action-suggestion-probe.js && node ./scripts/api-skill-match-probe.js",
|
|
56
56
|
"test:installed": "npm run test:installed-lifecycle && node ./scripts/installed-cleanroom-probe.js",
|
|
57
57
|
"test:installed-lifecycle": "node ./scripts/installed-handoff-lifecycle-probe.js",
|
|
58
58
|
"test:handoff": "node ./scripts/handoff-readonly-probe.js",
|
|
59
|
-
"test:fast": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && npm run test:state-scopes && npm run test:runtime-compatibility && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/e2e-repo-guard.js && npm run test:lock-heartbeat && npm run test:temp-cleanup && npm run test:path-setup && node ./scripts/platform-smoke.js && node ./scripts/parent-detect-selftest-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./scripts/i18n-priority-surface-probe.js && npm run test:harness-text && npm run test:test-plan && node ./scripts/i18n-next-cluster-probe.js && node ./scripts/handoff-i18n-probe.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/memory-guard-order-probe.js && node ./scripts/memory-input-preflight-probe.js && node ./scripts/memory-utf8-admission-probe.js && node ./scripts/memory-checked-input-probe.js && node ./scripts/memory-input-unit-probe.js && node ./scripts/memory-restore-canonical-probe.js && node ./scripts/memory-archive-retention-probe.js && node ./scripts/memory-archive-check-probe.js && node ./scripts/memory-projection-retention-probe.js && node ./scripts/memory-language-probe.js && node ./scripts/memory-legacy-retention-probe.js && node ./scripts/memory-recovery-receipt-probe.js && node ./scripts/memory-output-unit-probe.js && node ./scripts/memory-output-posix-probe.js && node ./scripts/memory-output-publication-probe.js && node ./scripts/memory-output-parent-race-probe.js && node ./scripts/mcp-presence-probe.js && node ./scripts/provider-capacity-probe.js && npm run test:provider-response && npm run test:provider-dispatch && npm run test:provider-receipt && npm run test:provider-observation && npm run test:ui-review-recovery && node ./scripts/file-lease-probe.js && node ./scripts/role-agent-schema-probe.js && node ./scripts/role-store-loader-probe.js && node ./scripts/role-fallback-probe.js && npm run test:agent-bench-output && npm run test:ledger-sync && npm run test:state-lock && npm run test:history-retention && node ./scripts/false-claim-probe.js && node ./scripts/smoke.js && node ./scripts/rule-applicability-probe.js && node ./scripts/review-checkpoint-probe.js && node ./scripts/review-checkpoint-transport-probe.js && node ./scripts/review-snapshot-probe.js && node ./scripts/review-snapshot-transport-probe.js && npm run test:memory-migration && npm run test:memory-items && npm run test:memory-budget && npm run test:memory-footprint && npm run test:memory-write-budget && npm run test:memory-write-preparation && npm run test:memory-write-admission && npm run test:memory-command-policy && npm run test:memory-publication-policy && npm run test:memory-recovery-transfer && npm run test:ui-boundary && npm run test:rule-verification",
|
|
59
|
+
"test:fast": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && npm run test:state-scopes && npm run test:runtime-compatibility && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/race-overlap-probe.js && node ./scripts/e2e-repo-guard.js && npm run test:lock-heartbeat && npm run test:temp-cleanup && npm run test:path-setup && npm run test:skill-archive && npm run test:memory-arena-cleanup && node ./scripts/platform-smoke.js && node ./scripts/parent-detect-selftest-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./scripts/i18n-priority-surface-probe.js && npm run test:harness-text && npm run test:test-plan && node ./scripts/i18n-next-cluster-probe.js && node ./scripts/handoff-i18n-probe.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/memory-guard-order-probe.js && node ./scripts/memory-input-preflight-probe.js && node ./scripts/memory-utf8-admission-probe.js && node ./scripts/memory-checked-input-probe.js && node ./scripts/memory-input-unit-probe.js && node ./scripts/memory-restore-canonical-probe.js && node ./scripts/memory-archive-retention-probe.js && node ./scripts/memory-archive-check-probe.js && node ./scripts/memory-projection-retention-probe.js && node ./scripts/memory-language-probe.js && node ./scripts/memory-legacy-retention-probe.js && node ./scripts/memory-recovery-receipt-probe.js && node ./scripts/memory-output-unit-probe.js && node ./scripts/memory-output-posix-probe.js && node ./scripts/memory-output-publication-probe.js && node ./scripts/memory-output-parent-race-probe.js && node ./scripts/mcp-presence-probe.js && node ./scripts/provider-capacity-probe.js && npm run test:provider-response && npm run test:provider-dispatch && npm run test:provider-receipt && npm run test:provider-observation && npm run test:ui-review-recovery && node ./scripts/file-lease-probe.js && node ./scripts/role-agent-schema-probe.js && node ./scripts/role-store-loader-probe.js && node ./scripts/role-fallback-probe.js && npm run test:agent-bench-output && npm run test:ledger-sync && npm run test:state-lock && npm run test:history-retention && node ./scripts/false-claim-probe.js && node ./scripts/smoke.js && node ./scripts/rule-applicability-probe.js && node ./scripts/review-checkpoint-probe.js && node ./scripts/review-checkpoint-transport-probe.js && node ./scripts/review-snapshot-probe.js && node ./scripts/review-snapshot-transport-probe.js && npm run test:memory-migration && npm run test:memory-items && npm run test:memory-budget && npm run test:memory-footprint && npm run test:memory-write-budget && npm run test:memory-write-preparation && npm run test:memory-write-admission && npm run test:memory-command-policy && npm run test:memory-publication-policy && npm run test:memory-recovery-transfer && npm run test:ui-boundary && npm run test:rule-verification",
|
|
60
60
|
"test:false-claims": "node ./scripts/false-claim-probe.js",
|
|
61
61
|
"test:claim-sweep": "node ./scripts/claim-sweep-git-probe.js",
|
|
62
62
|
"test:claim-runtime": "node ./scripts/claim-runtime-cache-probe.js",
|
|
@@ -74,6 +74,8 @@
|
|
|
74
74
|
"test:tracker-write-language": "node ./scripts/tracker-write-language-probe.js",
|
|
75
75
|
"test:write-path-language": "node ./scripts/write-path-language-probe.js",
|
|
76
76
|
"test:write-primitive-language": "node ./scripts/write-primitive-language-probe.js",
|
|
77
|
+
"test:race-overlap": "node ./scripts/race-overlap-probe.js",
|
|
78
|
+
"test:lazy-auto-track": "node ./scripts/lazy-auto-track-probe.js",
|
|
77
79
|
"test:dry-run-refusal": "node ./scripts/dry-run-refusal-probe.js",
|
|
78
80
|
"test:api-skill-match": "node ./scripts/api-skill-match-probe.js",
|
|
79
81
|
"test:crud-check": "node ./scripts/crud-check-probe.js",
|
|
@@ -141,6 +143,8 @@
|
|
|
141
143
|
"test:lock-heartbeat": "node ./scripts/lock-heartbeat-probe.js",
|
|
142
144
|
"test:temp-cleanup": "node ./scripts/temp-cleanup-probe.js && node ./scripts/copy-tree-probe.js",
|
|
143
145
|
"test:path-setup": "node ./scripts/path-setup-powershell-probe.js",
|
|
146
|
+
"test:skill-archive": "node ./scripts/skill-archive-probe.js",
|
|
147
|
+
"test:memory-arena-cleanup": "node ./scripts/memory-arena-cleanup-probe.js",
|
|
144
148
|
"test:ui-review-publication": "node ./scripts/ui-review-publication-probe.js",
|
|
145
149
|
"test:ui-review-publication-smoke": "node ./scripts/ui-review-publication-probe.js --smoke",
|
|
146
150
|
"test:ui-review-target": "node ./scripts/ui-review-target-probe.js",
|
|
@@ -41,7 +41,7 @@ const seed = lang => {
|
|
|
41
41
|
return (seeds[lang] = dir);
|
|
42
42
|
};
|
|
43
43
|
let copies = 0;
|
|
44
|
-
const fresh = (lang = 'ko') => { const dir = path.join(arena, 'p' + (++copies));
|
|
44
|
+
const fresh = (lang = 'ko') => { const dir = path.join(arena, 'p' + (++copies)); require('../lib/copy-tree').copyTreeSync(seed(lang), dir); return dir; };
|
|
45
45
|
const runIn = (cwd, args, runEnv = env) => cp.spawnSync(process.execPath, [CLI, ...args], { cwd, encoding: 'utf8', timeout: 300000, windowsHide: true, env: runEnv });
|
|
46
46
|
const show = r => `exit ${r.status}\n${String(r.stdout || '').slice(0, 1200)}${String(r.stderr || '').slice(0, 600)}`;
|
|
47
47
|
const json = (r, label) => { const out = String(r.stdout || ''); try { return JSON.parse(out.slice(out.indexOf('{'))); } catch { throw new Error(label + ': not JSON — ' + show(r)); } };
|
|
@@ -50,6 +50,23 @@ try {
|
|
|
50
50
|
assert.throws(() => copyTreeSync(from, to), error => error.code === 'EEXIST');
|
|
51
51
|
assert.strictEqual(fs.readFileSync(path.join(to, 'bytes.bin'), 'utf8'), 'kept');
|
|
52
52
|
});
|
|
53
|
+
// T-0347: a relative directory link must still name its sibling inside the copy. On Windows the copy is a junction,
|
|
54
|
+
// whose target must be absolute; Node resolved the relative one against the process cwd.
|
|
55
|
+
{
|
|
56
|
+
const from = path.join(arena, 'src-relative-link'), to = path.join(arena, 'dst-relative-link');
|
|
57
|
+
fs.mkdirSync(path.join(from, 'sibling'), { recursive: true });
|
|
58
|
+
fs.writeFileSync(path.join(from, 'sibling', 'mark.txt'), 'source');
|
|
59
|
+
let made = true;
|
|
60
|
+
try { fs.symlinkSync('sibling', path.join(from, 'link'), 'dir'); }
|
|
61
|
+
catch (error) { if (error.code !== 'EPERM') throw error; made = false; }
|
|
62
|
+
if (!made) console.log('SKIP a relative directory link (creating one needs the symlink privilege here)');
|
|
63
|
+
else test('a relative directory link names its sibling inside the copy', () => {
|
|
64
|
+
copyTreeSync(from, to);
|
|
65
|
+
fs.writeFileSync(path.join(to, 'sibling', 'mark.txt'), 'copy');
|
|
66
|
+
assert.strictEqual(fs.readFileSync(path.join(to, 'link', 'mark.txt'), 'utf8'), 'copy');
|
|
67
|
+
assert.strictEqual(fs.realpathSync.native(path.join(to, 'link')), fs.realpathSync.native(path.join(to, 'sibling')));
|
|
68
|
+
});
|
|
69
|
+
}
|
|
53
70
|
test('a missing source is an error, not an empty copy', () => {
|
|
54
71
|
assert.throws(() => copyTreeSync(path.join(arena, 'missing'), path.join(arena, 'dst-missing')), error => error.code === 'ENOENT');
|
|
55
72
|
assert.ok(!fs.existsSync(path.join(arena, 'dst-missing')));
|
|
@@ -34,7 +34,7 @@ const seed = () => {
|
|
|
34
34
|
return (seedDir = dir);
|
|
35
35
|
};
|
|
36
36
|
let copies = 0;
|
|
37
|
-
const fresh = () => { const dir = path.join(arena, 'p' + (++copies));
|
|
37
|
+
const fresh = () => { const dir = path.join(arena, 'p' + (++copies)); require('../lib/copy-tree').copyTreeSync(seed(), dir); return dir; };
|
|
38
38
|
const runIn = (cwd, args) => cp.spawnSync(process.execPath, [CLI, ...args], { cwd, encoding: 'utf8', timeout: 300000, windowsHide: true, env });
|
|
39
39
|
const show = r => `exit ${r.status}\n${String(r.stdout || '').slice(0, 1200)}${String(r.stderr || '').slice(0, 600)}`;
|
|
40
40
|
const json = (r, label) => { const out = String(r.stdout || ''); try { return JSON.parse(out.slice(out.indexOf('{'))); } catch { throw new Error(label + ': not JSON — ' + show(r)); } };
|
|
@@ -9,6 +9,7 @@ const cp = require('child_process');
|
|
|
9
9
|
|
|
10
10
|
const CLI = path.resolve(process.argv[2] || path.join(__dirname, '..', 'bin', 'leerness.js'));
|
|
11
11
|
const PRELOAD = path.join(__dirname, 'lock-probe.js');
|
|
12
|
+
const { overlapOf } = require('./race-overlap');
|
|
12
13
|
const ENV = {
|
|
13
14
|
...process.env,
|
|
14
15
|
LEERNESS_OFFLINE: '1',
|
|
@@ -37,16 +38,22 @@ function init(d) {
|
|
|
37
38
|
// is observed rather than inferred. T-0314: inferring it from `<target>.tmp-*` names stopped working when
|
|
38
39
|
// 1.36.190 published memory outputs through `.<target>.publish-*` + link; 8 of 11 races then never raced.
|
|
39
40
|
const readyFileOf = d => path.join(d, '.stall-ready.json');
|
|
41
|
+
// T-0346: the second command writes this file after it read the target, or tried its lock or a lock the first
|
|
42
|
+
// command held when it stalled; the first command's stall ends only after it exists (see scripts/lock-probe.js).
|
|
43
|
+
// A fixed 1.2 s stall could end before the second command had started (a Node start alone can take that long on
|
|
44
|
+
// Windows), and the race then passed without overlapping the publication.
|
|
45
|
+
const reachFileOf = d => path.join(d, '.stall-reached.json');
|
|
40
46
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const env =
|
|
46
|
-
? { ...ENV, LOCKPROBE_STALL_TARGET:
|
|
47
|
-
: ENV;
|
|
47
|
+
// role 'first' stalls the publication of `target` until the second command reaches it; role 'second' reports
|
|
48
|
+
// reaching it. Both run with the lock-probe preload.
|
|
49
|
+
function spawn(d, args, role, target) {
|
|
50
|
+
const nodeArgs = ['--require', PRELOAD, CLI, ...args, '--path', d];
|
|
51
|
+
const env = role === 'first'
|
|
52
|
+
? { ...ENV, LOCKPROBE_STALL_TARGET: target, LOCKPROBE_STALL_MS: String(STALL_MS), LOCKPROBE_READY_FILE: readyFileOf(d), LOCKPROBE_WAIT_FILE: reachFileOf(d) }
|
|
53
|
+
: { ...ENV, LOCKPROBE_REACH_TARGET: target, LOCKPROBE_REACH_FILE: reachFileOf(d), LOCKPROBE_REACH_LOCKS_FILE: readyFileOf(d) };
|
|
48
54
|
return cp.spawn(process.execPath, nodeArgs, { cwd: d, env, stdio: 'ignore' });
|
|
49
55
|
}
|
|
56
|
+
const readJson = file => { try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; } };
|
|
50
57
|
|
|
51
58
|
function waitForStall(d, target) {
|
|
52
59
|
// T-0343: an absolute target (a file outside .leerness) is acknowledged by its full path, a relative one by its
|
|
@@ -66,15 +73,22 @@ async function race(name, config) {
|
|
|
66
73
|
if (config.seed) config.seed(d);
|
|
67
74
|
// T-0343: a target outside .leerness is given as a function of the project directory (its absolute path).
|
|
68
75
|
const stallTarget = typeof config.stallTarget === 'function' ? config.stallTarget(d) : config.stallTarget;
|
|
69
|
-
const first = spawn(d, config.first, stallTarget);
|
|
76
|
+
const first = spawn(d, config.first, 'first', stallTarget);
|
|
70
77
|
const stalled = waitForStall(d, stallTarget);
|
|
71
78
|
if (config.beforeSecond) config.beforeSecond(d);
|
|
72
|
-
const second = spawn(d, typeof config.second === 'function' ? config.second(d) : config.second);
|
|
79
|
+
const second = spawn(d, typeof config.second === 'function' ? config.second(d) : config.second, 'second', stallTarget);
|
|
73
80
|
const exits = await Promise.all([wait(first), wait(second)]);
|
|
81
|
+
// T-0346: the race counts only when the second command overlapped the first one's stalled section (see
|
|
82
|
+
// scripts/race-overlap.js): a contended lock attempt, or, for a command that tried none of those locks, a read
|
|
83
|
+
// of the stalled file that finished before the first command resumed.
|
|
84
|
+
const ready = readJson(readyFileOf(d)) || {}, reach = readJson(reachFileOf(d));
|
|
85
|
+
const { overlapped, why } = overlapOf({ stalled, ready, reach });
|
|
74
86
|
const inspected = config.inspect(d);
|
|
75
87
|
// T-0343: `secondExit` names the second command's expected exit when it is not 0 (lazy detect reports issues with 1).
|
|
76
88
|
const secondExit = config.secondExit === undefined ? 0 : config.secondExit;
|
|
77
|
-
return { name, stalled,
|
|
89
|
+
return { name, stalled, overlapped, why, read: reach && reach.read && reach.read.how, lock: reach && reach.lock && reach.lock.result,
|
|
90
|
+
waitedMs: ready.waitedMs, exits, ...inspected,
|
|
91
|
+
ok: overlapped && exits[0] === 0 && exits[1] === secondExit && inspected.preserved === true };
|
|
78
92
|
} catch (e) {
|
|
79
93
|
return { name, error: String(e && e.message || e), ok: false };
|
|
80
94
|
} finally {
|
|
@@ -225,7 +225,7 @@ if (require.main === module) {
|
|
|
225
225
|
]) {
|
|
226
226
|
const copy = path.join(root, 'copy-' + (++n));
|
|
227
227
|
covered.push(what);
|
|
228
|
-
|
|
228
|
+
require('../lib/copy-tree').copyTreeSync(dir, copy);
|
|
229
229
|
t(`fixture: ${what} changed alone is the one reported missing (#${n})`, () => {
|
|
230
230
|
change(copy);
|
|
231
231
|
assert.deepStrictEqual(livedInMissing(copy).map(m => m.split(' (')[0]), [what]);
|
|
@@ -235,7 +235,7 @@ if (require.main === module) {
|
|
|
235
235
|
// Control for the whole-id match: the id in Markdown emphasis still names the task.
|
|
236
236
|
t('fixture: a handoff naming the task as __id__ still counts', () => {
|
|
237
237
|
const copy = path.join(root, 'copy-emphasis');
|
|
238
|
-
|
|
238
|
+
require('../lib/copy-tree').copyTreeSync(dir, copy);
|
|
239
239
|
edit(copy, 'session-handoff.md', built.ids.task1, '__' + built.ids.task1 + '__');
|
|
240
240
|
assert.deepStrictEqual(livedInMissing(copy), []);
|
|
241
241
|
});
|
|
@@ -31,7 +31,7 @@ const seed = lang => {
|
|
|
31
31
|
return (seeds[lang] = dir);
|
|
32
32
|
};
|
|
33
33
|
let copies = 0;
|
|
34
|
-
const fresh = lang => { const dir = path.join(arena, 'p' + (++copies));
|
|
34
|
+
const fresh = lang => { const dir = path.join(arena, 'p' + (++copies)); require('../lib/copy-tree').copyTreeSync(seed(lang), dir); return dir; };
|
|
35
35
|
const runIn = (cwd, args, extraEnv = {}) => cp.spawnSync(process.execPath, [CLI, ...args], { cwd, encoding: 'utf8', timeout: 300000, windowsHide: true, env: { ...env, ...extraEnv } });
|
|
36
36
|
const show = r => `exit ${r.status}\n${String(r.stdout || '').slice(0, 1500)}${String(r.stderr || '').slice(0, 600)}`;
|
|
37
37
|
const addDone = (dir, title, evidence) => {
|
|
@@ -140,7 +140,7 @@ function normalizedJson(label, result) {
|
|
|
140
140
|
function cloneProjectState(target) {
|
|
141
141
|
fs.mkdirSync(target, { recursive: true });
|
|
142
142
|
fs.copyFileSync(path.join(project, 'package.json'), path.join(target, 'package.json'));
|
|
143
|
-
|
|
143
|
+
require('../lib/copy-tree').copyTreeSync(path.join(project, '.leerness'), path.join(target, '.leerness'));
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
const surfaces = [
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// T-0345: `lazy detect --auto-track` registers a task for each new TODO whose file no tracker row references, and
|
|
3
|
+
// only those. A batch that mixed such files with a file a task already referenced registered every new TODO, so the
|
|
4
|
+
// TODOs in the referenced file got tasks of their own as well. known-todos.json gains exactly the registered TODOs.
|
|
5
|
+
const assert = require('assert');
|
|
6
|
+
const cp = require('child_process');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const CLI = path.resolve(__dirname, '..', 'bin', 'leerness.js');
|
|
12
|
+
const ENV = { ...process.env, LEERNESS_OFFLINE: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_LOCK_WARN: '1', LEERNESS_NO_AUTOCHCP: '1' };
|
|
13
|
+
const arena = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-lazy-auto-track-'));
|
|
14
|
+
let passed = 0, failed = 0, seq = 0;
|
|
15
|
+
function test(name, fn) {
|
|
16
|
+
try { fn(); passed++; console.log('PASS ' + name); }
|
|
17
|
+
catch (error) { failed++; console.error('FAIL ' + name + ': ' + (error && error.stack || error)); }
|
|
18
|
+
}
|
|
19
|
+
const run = (d, args) => {
|
|
20
|
+
const r = cp.spawnSync(process.execPath, [CLI, ...args], { cwd: d, env: ENV, encoding: 'utf8', timeout: 120000, windowsHide: true });
|
|
21
|
+
assert.strictEqual(r.error, undefined, r.error && r.error.message);
|
|
22
|
+
return r;
|
|
23
|
+
};
|
|
24
|
+
// A project with the given source files and, for each entry of `referenced`, a task whose text names that file.
|
|
25
|
+
function project(files, referenced = []) {
|
|
26
|
+
const d = path.join(arena, 'p' + (++seq));
|
|
27
|
+
fs.mkdirSync(d);
|
|
28
|
+
assert.strictEqual(run(d, ['init', d, '--yes', '--minimal']).status, 0, 'init');
|
|
29
|
+
for (const [name, text] of Object.entries(files)) {
|
|
30
|
+
fs.mkdirSync(path.dirname(path.join(d, name)), { recursive: true });
|
|
31
|
+
fs.writeFileSync(path.join(d, name), text);
|
|
32
|
+
}
|
|
33
|
+
for (const name of referenced) assert.strictEqual(run(d, ['task', 'add', `Clean up ${name}`, '--path', d]).status, 0, 'task add');
|
|
34
|
+
return d;
|
|
35
|
+
}
|
|
36
|
+
const tasks = d => {
|
|
37
|
+
const r = run(d, ['task', 'list', '--path', d, '--json']);
|
|
38
|
+
assert.strictEqual(r.status, 0, r.stdout + r.stderr);
|
|
39
|
+
const parsed = JSON.parse(r.stdout);
|
|
40
|
+
return (parsed.tasks || parsed).map(t => t.request);
|
|
41
|
+
};
|
|
42
|
+
const autoTracked = d => tasks(d).filter(request => /^TODO /.test(request)).sort();
|
|
43
|
+
const known = d => {
|
|
44
|
+
const file = path.join(d, '.leerness', 'known-todos.json');
|
|
45
|
+
return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')).map(t => `${t.file}:${t.line}`).sort() : null;
|
|
46
|
+
};
|
|
47
|
+
const autoTrack = d => {
|
|
48
|
+
const r = run(d, ['lazy', 'detect', d, '--auto-track']);
|
|
49
|
+
assert.ok(r.status === 0 || r.status === 1, 'lazy detect exit ' + r.status + '\n' + r.stdout + r.stderr);
|
|
50
|
+
return r;
|
|
51
|
+
};
|
|
52
|
+
const SOURCES = {
|
|
53
|
+
'src/covered.js': '// TODO: first covered note\nfunction a() {}\n// FIXME: second covered note\n',
|
|
54
|
+
'src/fresh.js': 'function b() {}\n// TODO: fresh note\n',
|
|
55
|
+
};
|
|
56
|
+
try {
|
|
57
|
+
test('a mixed batch registers only the TODO in the file no task references', () => {
|
|
58
|
+
const d = project(SOURCES, ['src/covered.js']);
|
|
59
|
+
autoTrack(d);
|
|
60
|
+
assert.deepStrictEqual(autoTracked(d), ['TODO src/fresh.js:2']);
|
|
61
|
+
assert.deepStrictEqual(known(d), ['src/fresh.js:2']);
|
|
62
|
+
assert.strictEqual(tasks(d).filter(request => request === 'Clean up src/covered.js').length, 1, 'the referencing task is kept as it was');
|
|
63
|
+
});
|
|
64
|
+
test('running it again registers nothing more', () => {
|
|
65
|
+
const d = project(SOURCES, ['src/covered.js']);
|
|
66
|
+
autoTrack(d);
|
|
67
|
+
autoTrack(d);
|
|
68
|
+
assert.deepStrictEqual(autoTracked(d), ['TODO src/fresh.js:2']);
|
|
69
|
+
assert.deepStrictEqual(known(d), ['src/fresh.js:2']);
|
|
70
|
+
});
|
|
71
|
+
test('control: with no file referenced, every new TODO is registered', () => {
|
|
72
|
+
const d = project(SOURCES);
|
|
73
|
+
autoTrack(d);
|
|
74
|
+
assert.deepStrictEqual(autoTracked(d), ['TODO src/covered.js:1', 'TODO src/covered.js:3', 'TODO src/fresh.js:2']);
|
|
75
|
+
assert.deepStrictEqual(known(d), ['src/covered.js:1', 'src/covered.js:3', 'src/fresh.js:2']);
|
|
76
|
+
});
|
|
77
|
+
test('control: with every file referenced, nothing is registered and known-todos.json is not written', () => {
|
|
78
|
+
const d = project(SOURCES, ['src/covered.js', 'src/fresh.js']);
|
|
79
|
+
autoTrack(d);
|
|
80
|
+
assert.deepStrictEqual(autoTracked(d), []);
|
|
81
|
+
assert.strictEqual(known(d), null);
|
|
82
|
+
});
|
|
83
|
+
} finally {
|
|
84
|
+
fs.rmSync(arena, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
console.log(`lazy auto-track probe: ${passed}/${passed + failed} passed (${process.platform})`);
|
|
87
|
+
process.exitCode = failed ? 1 : 0;
|