gitnexus 1.6.12-rc.39 → 1.6.12-rc.40
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/dist/core/ingestion/pipeline-phases/parse-impl.js +32 -6
- package/dist/core/run-analyze.js +5 -3
- package/dist/storage/parse-cache.d.ts +22 -0
- package/dist/storage/parse-cache.js +25 -1
- package/dist/storage/parsedfile-store.d.ts +15 -2
- package/dist/storage/parsedfile-store.js +59 -5
- package/package.json +1 -1
- package/scripts/cross-platform-tests.ts +5 -0
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { BindingAccumulator, enrichExportedTypeMap, } from '../binding-accumulator.js';
|
|
18
18
|
import { mergeChunkResults, dispatchChunkParseRound } from '../parsing-processor.js';
|
|
19
|
-
import { fileContentHash, computeChunkHash, loadParseCacheChunk, persistParseCacheChunk, PARSE_CACHE_VERSION, packParseCacheChunks, } from '../../../storage/parse-cache.js';
|
|
20
|
-
import { clearParsedFileStore, persistParsedFileChunk, loadParsedFilesForPaths, getDurableParsedFileDir, loadDurableParsedFileIndex, prepareDurableParsedFileChunk, durableChunkHasShards, } from '../../../storage/parsedfile-store.js';
|
|
19
|
+
import { fileContentHash, computeChunkHash, loadParseCacheChunk, persistParseCacheChunk, markParseCacheChunkStale, PARSE_CACHE_VERSION, packParseCacheChunks, } from '../../../storage/parse-cache.js';
|
|
20
|
+
import { clearParsedFileStore, persistParsedFileChunk, loadParsedFilesForPaths, getDurableParsedFileDir, loadDurableParsedFileIndex, prepareDurableParsedFileChunk, durableChunkHasShards, durableChunkHasStaleShards, } from '../../../storage/parsedfile-store.js';
|
|
21
21
|
import { DEFAULT_PDG_MAX_FUNCTION_LINES } from '../cfg/collect.js';
|
|
22
22
|
import { processRoutesFromExtracted, resolveRouteHandlerSymbols, buildExportedTypeMapFromGraph, } from '../call-processor.js';
|
|
23
23
|
import { createSemanticModel } from '../model/index.js';
|
|
@@ -669,8 +669,12 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
669
669
|
* Chunk hashes whose durable ParsedFile directory could not be reset. The
|
|
670
670
|
* old generation's shards are still on disk, so a warm hit would union
|
|
671
671
|
* stale shards with the new ones. Treated exactly like a quarantined chunk:
|
|
672
|
-
* skip the parse-cache write
|
|
673
|
-
*
|
|
672
|
+
* skip the parse-cache write AND, when shards from that generation are
|
|
673
|
+
* still on disk, retire the hash (#3204): the old `.v8` is not carried
|
|
674
|
+
* forward and the directory is dropped from the durable index, so the next
|
|
675
|
+
* run re-dispatches. Kept as its own set rather than
|
|
676
|
+
* read back off `staleKeys`, which is a superset — it is what selects the
|
|
677
|
+
* warn below over the quarantine branch's dev-only log.
|
|
674
678
|
*/
|
|
675
679
|
const durablePrepareFailures = new Set();
|
|
676
680
|
const roundByteBudget = resolveParseRoundByteBudget(options);
|
|
@@ -809,7 +813,11 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
809
813
|
// Persist raw results for this chunk hash (skipping when any chunk file
|
|
810
814
|
// was worker-quarantined, so the narrower rawResults isn't cached under
|
|
811
815
|
// the full-chunk key — see the original inline note / U20.U2).
|
|
812
|
-
|
|
816
|
+
// `rawResults.length > 0` guards the WRITE only. A quarantined chunk
|
|
817
|
+
// often returns nothing at all (the worker died on it), and that chunk
|
|
818
|
+
// still has to be retired — otherwise its pre-existing `.v8` is copied
|
|
819
|
+
// forward at save time (#3204).
|
|
820
|
+
if (parseCache && p.chunkHash) {
|
|
813
821
|
const quarantineSet = new Set(workerPool?.getQuarantinedPaths?.() ?? []);
|
|
814
822
|
const chunkHadQuarantine = p.chunkFiles.some((f) => quarantineSet.has(f.path));
|
|
815
823
|
const durableGenerationStale = durablePrepareFailures.has(p.chunkHash);
|
|
@@ -818,6 +826,10 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
818
826
|
'so its shards may be stale; next run will re-dispatch it');
|
|
819
827
|
}
|
|
820
828
|
else if (chunkHadQuarantine) {
|
|
829
|
+
// This chunk's durable directory now holds only this run's NARROWER
|
|
830
|
+
// shards, so a warm hit would replay the full-coverage `.v8` over
|
|
831
|
+
// partial ParsedFiles (#3204).
|
|
832
|
+
markParseCacheChunkStale(parseCache, p.chunkHash);
|
|
821
833
|
if (isDev) {
|
|
822
834
|
const quarantinedInChunk = p.chunkFiles.filter((f) => quarantineSet.has(f.path)).length;
|
|
823
835
|
logger.info(`📦 parse-cache SKIP: chunk ${p.chunkIdx + 1}/${numChunks} ` +
|
|
@@ -825,7 +837,7 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
825
837
|
`next run will rediscover (${p.chunkHash.slice(0, 8)})`);
|
|
826
838
|
}
|
|
827
839
|
}
|
|
828
|
-
else {
|
|
840
|
+
else if (rawResults.length > 0) {
|
|
829
841
|
await persistParseCacheChunk(parseCache, p.chunkHash, rawResults);
|
|
830
842
|
if (isDev) {
|
|
831
843
|
logger.info(`📦 parse-cache MISS+store: chunk ${p.chunkIdx + 1}/${numChunks} (${p.chunkFiles.length} files, ${p.chunkHash.slice(0, 8)})`);
|
|
@@ -871,6 +883,20 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
871
883
|
// directory on write, so at worst the old generation lingers.
|
|
872
884
|
// Caught per chunk so one failure cannot abort the others.
|
|
873
885
|
durablePrepareFailures.add(miss.chunkHash);
|
|
886
|
+
// Retire ONLY when a generation nobody cleared is still on disk.
|
|
887
|
+
// `prepareDurableParsedFileChunk` is rm-then-mkdir: an rm failure
|
|
888
|
+
// leaves the old shards to be unioned into a warm hit, but an rm
|
|
889
|
+
// that succeeded before a failing mkdir leaves nothing — the
|
|
890
|
+
// workers recreate the directory and write a clean generation, so
|
|
891
|
+
// retiring there would discard a good `.v8` for no safety gain
|
|
892
|
+
// (and a correlated burst would discard the whole shared cache).
|
|
893
|
+
// Retire here rather than at finalize: that branch sits behind
|
|
894
|
+
// `rawResults.length > 0`, so a chunk whose worker round returns
|
|
895
|
+
// nothing would keep its stale entry.
|
|
896
|
+
if (parseCache &&
|
|
897
|
+
(await durableChunkHasStaleShards(durableParsedFileDir, miss.chunkHash))) {
|
|
898
|
+
markParseCacheChunkStale(parseCache, miss.chunkHash);
|
|
899
|
+
}
|
|
874
900
|
logger.warn({ err, chunkHash: miss.chunkHash.slice(0, 8) }, 'parsedfile-cache: could not reset durable chunk generation; ' +
|
|
875
901
|
'continuing without caching this chunk');
|
|
876
902
|
}
|
package/dist/core/run-analyze.js
CHANGED
|
@@ -3437,9 +3437,11 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
3437
3437
|
// Prune the durable ParsedFile store to EXACTLY the parse cache's
|
|
3438
3438
|
// surviving keys (#2038 warm-cache coverage), so the two content-addressed
|
|
3439
3439
|
// stores stay coherent: a chunk is "cached" iff both its parse-cache shard
|
|
3440
|
-
// and its durable shards exist. A
|
|
3441
|
-
//
|
|
3442
|
-
//
|
|
3440
|
+
// and its durable shards exist. A retired chunk — worker-quarantined, or
|
|
3441
|
+
// one whose failed durable reset left an uncleared generation behind
|
|
3442
|
+
// (#3204) — is filtered out of `savedKeys`, so it drops out of the
|
|
3443
|
+
// durable index here and re-dispatches next run. Same try/catch — a
|
|
3444
|
+
// durable-store write must never
|
|
3443
3445
|
// break an otherwise successful run (next run treats it as a miss).
|
|
3444
3446
|
await mergeStagedDurableParsedFileStore(storagePath, parseCache.storagePath ?? storagePath, PARSE_CACHE_VERSION, new Set(savedKeys));
|
|
3445
3447
|
}
|
|
@@ -68,6 +68,16 @@ export interface ParseCache {
|
|
|
68
68
|
* Transient — never serialized to disk.
|
|
69
69
|
*/
|
|
70
70
|
usedKeys: Set<string>;
|
|
71
|
+
/**
|
|
72
|
+
* Hashes this run decided it cannot vouch for — its durable generation could
|
|
73
|
+
* not be reset, or its chunk was worker-quarantined (#3204). `saveParseCache`
|
|
74
|
+
* refuses them, so neither a pre-existing `.v8` nor the chunk's durable
|
|
75
|
+
* directory survives into the next run. Kept separate from `usedKeys`
|
|
76
|
+
* because the orchestrator re-adds keys to that set after the parse phase
|
|
77
|
+
* (#2106 sibling fold), which would undo a deletion.
|
|
78
|
+
* Transient — never serialized to disk.
|
|
79
|
+
*/
|
|
80
|
+
staleKeys?: Set<string>;
|
|
71
81
|
/**
|
|
72
82
|
* When set, chunk payloads are loaded from / flushed to sharded files on
|
|
73
83
|
* demand instead of retaining every chunk in `entries` for the whole run
|
|
@@ -129,6 +139,18 @@ export declare const forgetCreatedParseCacheDir: (storagePath: string) => void;
|
|
|
129
139
|
* run. Falls back to `cache.entries` when `storagePath` is unset (unit tests).
|
|
130
140
|
*/
|
|
131
141
|
export declare const persistParseCacheChunk: (cache: ParseCache, chunkHash: string, chunkResults: readonly ParseWorkerResult[]) => Promise<void>;
|
|
142
|
+
/**
|
|
143
|
+
* Retire a chunk this run cannot vouch for — its durable ParsedFile generation
|
|
144
|
+
* could not be reset, or its chunk was worker-quarantined (#3204).
|
|
145
|
+
*
|
|
146
|
+
* `saveParseCache` refuses a stale key, so no pre-existing `.v8` is copied
|
|
147
|
+
* forward and the durable store — pruned to exactly the keys that save
|
|
148
|
+
* returns — drops the chunk in the same step. The two deletes matter because
|
|
149
|
+
* `loadParseCacheChunk` reads `entries` and `onDiskKeys` and does NOT consult
|
|
150
|
+
* `staleKeys`: without them a second lookup of the same hash inside this run
|
|
151
|
+
* would still serve the retired shard.
|
|
152
|
+
*/
|
|
153
|
+
export declare const markParseCacheChunkStale: (cache: ParseCache, chunkHash: string) => void;
|
|
132
154
|
/**
|
|
133
155
|
* Load the parse cache. Returns an empty cache on any failure (missing
|
|
134
156
|
* file, corrupt JSON, version mismatch). Never throws on a normal load.
|
|
@@ -1017,6 +1017,23 @@ export const persistParseCacheChunk = async (cache, chunkHash, chunkResults) =>
|
|
|
1017
1017
|
}
|
|
1018
1018
|
cache.entries.set(chunkHash, slim);
|
|
1019
1019
|
};
|
|
1020
|
+
/**
|
|
1021
|
+
* Retire a chunk this run cannot vouch for — its durable ParsedFile generation
|
|
1022
|
+
* could not be reset, or its chunk was worker-quarantined (#3204).
|
|
1023
|
+
*
|
|
1024
|
+
* `saveParseCache` refuses a stale key, so no pre-existing `.v8` is copied
|
|
1025
|
+
* forward and the durable store — pruned to exactly the keys that save
|
|
1026
|
+
* returns — drops the chunk in the same step. The two deletes matter because
|
|
1027
|
+
* `loadParseCacheChunk` reads `entries` and `onDiskKeys` and does NOT consult
|
|
1028
|
+
* `staleKeys`: without them a second lookup of the same hash inside this run
|
|
1029
|
+
* would still serve the retired shard.
|
|
1030
|
+
*/
|
|
1031
|
+
export const markParseCacheChunkStale = (cache, chunkHash) => {
|
|
1032
|
+
cache.staleKeys ??= new Set();
|
|
1033
|
+
cache.staleKeys.add(chunkHash);
|
|
1034
|
+
cache.entries.delete(chunkHash);
|
|
1035
|
+
cache.onDiskKeys?.delete(chunkHash);
|
|
1036
|
+
};
|
|
1020
1037
|
const loadLegacyParseCache = async (storagePath) => {
|
|
1021
1038
|
const cachePath = getLegacyCachePath(storagePath);
|
|
1022
1039
|
try {
|
|
@@ -1096,7 +1113,14 @@ export const saveParseCache = async (storagePath, cache) => {
|
|
|
1096
1113
|
const tmpDir = `${cacheDir}.tmp`;
|
|
1097
1114
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
1098
1115
|
await fs.mkdir(tmpDir, { recursive: true });
|
|
1099
|
-
|
|
1116
|
+
// A stale key is dropped here rather than at the failure site: the
|
|
1117
|
+
// orchestrator folds sibling-branch keys back into `usedKeys` after the parse
|
|
1118
|
+
// phase (#2106), so this is the last point that sees the final key set. The
|
|
1119
|
+
// exclusion also reaches the durable store, which prunes to the keys this
|
|
1120
|
+
// function returns — both stores drop the chunk together (#3204).
|
|
1121
|
+
const keys = [...cache.usedKeys]
|
|
1122
|
+
.filter((key) => isValidChunkCacheKey(key) && !cache.staleKeys?.has(key))
|
|
1123
|
+
.sort();
|
|
1100
1124
|
// Track hashes whose shard was actually written/copied this save. A hash can
|
|
1101
1125
|
// be in `usedKeys` without a backing shard — its in-memory serialize threw, or
|
|
1102
1126
|
// its on-disk copy failed/was-absent (e.g. a worker-quarantined chunk added to
|
|
@@ -103,6 +103,18 @@ export declare const getDurableParsedFileDir: (storagePath: string) => string;
|
|
|
103
103
|
* from accumulating across analyses.
|
|
104
104
|
*/
|
|
105
105
|
export declare const prepareDurableParsedFileChunk: (durableDir: string, chunkHash: string) => Promise<void>;
|
|
106
|
+
/**
|
|
107
|
+
* Does this chunk still hold shards from a generation nobody cleared?
|
|
108
|
+
*
|
|
109
|
+
* Asked only after {@link prepareDurableParsedFileChunk} rejected, to tell its
|
|
110
|
+
* two failure modes apart (#3204). The `rm` failing leaves the previous
|
|
111
|
+
* generation in place, and a warm hit would union it with whatever this run's
|
|
112
|
+
* workers write — that chunk must be retired. The `rm` succeeding and the
|
|
113
|
+
* `mkdir` then failing leaves NO directory: the workers recreate it and write
|
|
114
|
+
* a clean generation, so retiring would throw away a good cache entry for
|
|
115
|
+
* nothing. Absent or empty ⇒ nothing to distrust.
|
|
116
|
+
*/
|
|
117
|
+
export declare const durableChunkHasStaleShards: (durableDir: string, chunkHash: string) => Promise<boolean>;
|
|
106
118
|
export declare const persistDurableParsedFileShardSync: (durableDir: string, chunkHash: string, threadId: number, shardSeq: number, parsedFiles: readonly ParsedFile[]) => boolean;
|
|
107
119
|
/**
|
|
108
120
|
* Validate and snapshot one durable chunk into the run store. Every envelope
|
|
@@ -116,8 +128,9 @@ export declare const loadDurableParsedFileIndex: (durableDir: string, expectedVe
|
|
|
116
128
|
* Prune the durable store to `keepKeys` and rewrite its index. `keepKeys` must
|
|
117
129
|
* be the parse cache's surviving on-disk keys (so the two stores stay coherent:
|
|
118
130
|
* a chunk is "cached" iff BOTH its parse-cache shard and its durable shards
|
|
119
|
-
* exist; a
|
|
120
|
-
*
|
|
131
|
+
* exist; a chunk retired by `markParseCacheChunkStale` — worker-quarantined, or
|
|
132
|
+
* holding a durable generation that could not be reset — is absent from
|
|
133
|
+
* `keepKeys`, so it drops its durable subdir here and re-dispatches next run). Only chunks whose envelopes all validate
|
|
121
134
|
* are indexed, together with their exact persisted path coverage (never vouch
|
|
122
135
|
* for a missing/corrupt shard). The index write is tmp+rename atomic.
|
|
123
136
|
*/
|
|
@@ -512,6 +512,18 @@ export const prepareDurableParsedFileChunk = async (durableDir, chunkHash) => {
|
|
|
512
512
|
await fs.rm(dir, { recursive: true, force: true });
|
|
513
513
|
await fs.mkdir(dir, { recursive: true });
|
|
514
514
|
};
|
|
515
|
+
/**
|
|
516
|
+
* Does this chunk still hold shards from a generation nobody cleared?
|
|
517
|
+
*
|
|
518
|
+
* Asked only after {@link prepareDurableParsedFileChunk} rejected, to tell its
|
|
519
|
+
* two failure modes apart (#3204). The `rm` failing leaves the previous
|
|
520
|
+
* generation in place, and a warm hit would union it with whatever this run's
|
|
521
|
+
* workers write — that chunk must be retired. The `rm` succeeding and the
|
|
522
|
+
* `mkdir` then failing leaves NO directory: the workers recreate it and write
|
|
523
|
+
* a clean generation, so retiring would throw away a good cache entry for
|
|
524
|
+
* nothing. Absent or empty ⇒ nothing to distrust.
|
|
525
|
+
*/
|
|
526
|
+
export const durableChunkHasStaleShards = async (durableDir, chunkHash) => (await listV8Shards(durableChunkDir(durableDir, chunkHash))).length > 0;
|
|
515
527
|
/**
|
|
516
528
|
* Synchronous durable-shard writer for use INSIDE a parse worker, alongside
|
|
517
529
|
* {@link persistParsedFileShardSync}. Writes the SAME bytes to a content-addressed
|
|
@@ -606,8 +618,9 @@ export const loadDurableParsedFileIndex = async (durableDir, expectedVersion) =>
|
|
|
606
618
|
* Prune the durable store to `keepKeys` and rewrite its index. `keepKeys` must
|
|
607
619
|
* be the parse cache's surviving on-disk keys (so the two stores stay coherent:
|
|
608
620
|
* a chunk is "cached" iff BOTH its parse-cache shard and its durable shards
|
|
609
|
-
* exist; a
|
|
610
|
-
*
|
|
621
|
+
* exist; a chunk retired by `markParseCacheChunkStale` — worker-quarantined, or
|
|
622
|
+
* holding a durable generation that could not be reset — is absent from
|
|
623
|
+
* `keepKeys`, so it drops its durable subdir here and re-dispatches next run). Only chunks whose envelopes all validate
|
|
611
624
|
* are indexed, together with their exact persisted path coverage (never vouch
|
|
612
625
|
* for a missing/corrupt shard). The index write is tmp+rename atomic.
|
|
613
626
|
*/
|
|
@@ -620,6 +633,8 @@ export const pruneAndSaveDurableParsedFileStore = async (durableDir, version, ke
|
|
|
620
633
|
return; // nothing written this run
|
|
621
634
|
}
|
|
622
635
|
const survivors = {};
|
|
636
|
+
const undeletable = [];
|
|
637
|
+
let firstRemoveError;
|
|
623
638
|
for (const name of entries) {
|
|
624
639
|
if (name === DURABLE_INDEX_FILENAME)
|
|
625
640
|
continue;
|
|
@@ -649,7 +664,27 @@ export const pruneAndSaveDurableParsedFileStore = async (durableDir, version, ke
|
|
|
649
664
|
/* not a readable dir → drop below */
|
|
650
665
|
}
|
|
651
666
|
}
|
|
652
|
-
|
|
667
|
+
// The causes that break `prepareDurableParsedFileChunk` — permissions, a
|
|
668
|
+
// locked file, a read-only mount — break this rm too (#3204). Dropping the
|
|
669
|
+
// entry from the index is what makes the chunk unreachable; losing the
|
|
670
|
+
// directory is a cleanup bonus. Never let one of them abort the loop and
|
|
671
|
+
// cost every remaining chunk its index entry.
|
|
672
|
+
try {
|
|
673
|
+
await fs.rm(full, { recursive: true, force: true });
|
|
674
|
+
}
|
|
675
|
+
catch (err) {
|
|
676
|
+
// `name` reached the drop branch precisely because it is not a live
|
|
677
|
+
// chunk key, so it may be any stray directory — report it as an entry.
|
|
678
|
+
undeletable.push(name);
|
|
679
|
+
firstRemoveError ??= err;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (undeletable.length > 0) {
|
|
683
|
+
// One line per RUN, not per directory: a store-wide cause (read-only mount,
|
|
684
|
+
// wrong ownership) hits every non-survivor, and thousands of warns would
|
|
685
|
+
// bury the message that matters.
|
|
686
|
+
logger.warn({ err: firstRemoveError, count: undeletable.length, firstEntry: undeletable[0] }, 'parsedfile-cache: could not remove pruned durable chunk directories; ' +
|
|
687
|
+
'they are excluded from the index and will be re-attempted next run');
|
|
653
688
|
}
|
|
654
689
|
const idx = { version, entries: survivors };
|
|
655
690
|
const tmp = path.join(durableDir, `${DURABLE_INDEX_FILENAME}.tmp`);
|
|
@@ -684,7 +719,18 @@ export const mergeStagedDurableParsedFileStore = async (liveStoragePath, stagedS
|
|
|
684
719
|
continue;
|
|
685
720
|
const from = path.join(stagedDir, name);
|
|
686
721
|
const to = path.join(liveDir, name);
|
|
687
|
-
|
|
722
|
+
// Same reasoning as the prune's per-entry guard, on the loop that runs
|
|
723
|
+
// BEFORE it (#3204): this overlay targets the same live chunk directories,
|
|
724
|
+
// so the causes that break a reset break a replacement too. Letting one
|
|
725
|
+
// throw here would skip the prune entirely — the durable index would not be
|
|
726
|
+
// rewritten this run, and a retired chunk would keep its directory.
|
|
727
|
+
try {
|
|
728
|
+
await replaceDurableChunkDir(from, to);
|
|
729
|
+
}
|
|
730
|
+
catch (err) {
|
|
731
|
+
logger.warn({ err, entry: name }, 'parsedfile-cache: could not publish a staged durable chunk; ' +
|
|
732
|
+
'it stays uncached and will re-dispatch next run');
|
|
733
|
+
}
|
|
688
734
|
}
|
|
689
735
|
await pruneAndSaveDurableParsedFileStore(liveDir, version, keepKeys);
|
|
690
736
|
};
|
|
@@ -698,6 +744,12 @@ const replaceDurableChunkDir = async (from, to) => {
|
|
|
698
744
|
/* dest exists, or the rename is cross-device */
|
|
699
745
|
}
|
|
700
746
|
const backup = `${to}.replacing`;
|
|
747
|
+
// Deliberately NOT best-effort. If a non-empty backup survives, the
|
|
748
|
+
// `fs.rename(to, backup)` below cannot overwrite it and is swallowed as
|
|
749
|
+
// "dest was missing", so the `fs.cp` fallback would merge the staged
|
|
750
|
+
// generation INTO the live directory — manufacturing exactly the old+new
|
|
751
|
+
// union this fix exists to prevent. Let it throw; the caller's per-entry
|
|
752
|
+
// guard keeps one such chunk from costing the others their prune.
|
|
701
753
|
await fs.rm(backup, { recursive: true, force: true });
|
|
702
754
|
let backedUp = false;
|
|
703
755
|
try {
|
|
@@ -724,6 +776,8 @@ const replaceDurableChunkDir = async (from, to) => {
|
|
|
724
776
|
throw err;
|
|
725
777
|
}
|
|
726
778
|
if (backedUp) {
|
|
727
|
-
|
|
779
|
+
// The new generation is already in place; an undeletable backup is litter,
|
|
780
|
+
// not a failure. The next prune re-attempts it.
|
|
781
|
+
await fs.rm(backup, { recursive: true, force: true }).catch(() => { });
|
|
728
782
|
}
|
|
729
783
|
};
|
package/package.json
CHANGED
|
@@ -290,6 +290,11 @@ const NATIVE_ADDON_SMOKE = [
|
|
|
290
290
|
// Filesystem behavior tests — exercise operations that vary across
|
|
291
291
|
// platforms (CRLF, symlinks, permissions, temp dirs)
|
|
292
292
|
const FILESYSTEM = [
|
|
293
|
+
// The durable ParsedFile store's prune tolerates a chunk directory it cannot
|
|
294
|
+
// delete (#3204). The failures that motivate it — held handles, read-only
|
|
295
|
+
// mounts — are Windows- and macOS-flavored, and the permission-based case
|
|
296
|
+
// skips itself where chmod cannot block a delete, so run it everywhere.
|
|
297
|
+
'test/unit/parsedfile-store.test.ts',
|
|
293
298
|
'test/integration/filesystem-walker.test.ts',
|
|
294
299
|
'test/integration/watch-filesystem.test.ts',
|
|
295
300
|
'test/integration/markdown-processor-crlf.test.ts',
|