Skip to content

Commit 7918f6b

Browse files
[Improve] Strengthen managed binary installation (#1059)
* refactor: extract managed binary installation infrastructure Refs #1055 * fix: address managed binary review feedback * fix: address managed binary review feedback * test: cover managed binary cleanup boundaries * fix: finalize managed binary download handling * test: mirror download stream close events --------- Co-authored-by: Naved Merchant <14171946+navedmerchant@users.noreply.github.com>
1 parent 0540104 commit 7918f6b

9 files changed

Lines changed: 1067 additions & 447 deletions

File tree

src/eslint-suppressions.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1484,11 +1484,6 @@
14841484
"count": 3
14851485
}
14861486
},
1487-
"services/code-index/semble/semble-downloader.ts": {
1488-
"@typescript-eslint/no-explicit-any": {
1489-
"count": 1
1490-
}
1491-
},
14921487
"services/code-index/shared/__tests__/validation-helpers.spec.ts": {
14931488
"@typescript-eslint/no-explicit-any": {
14941489
"count": 4

src/services/code-index/semble/__tests__/semble-downloader.spec.ts

Lines changed: 41 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,20 @@ vi.mock("fs/promises", () => ({
3232
readdir: vi.fn().mockResolvedValue([]),
3333
}))
3434

35+
vi.mock("proper-lockfile", () => ({
36+
lock: vi.fn().mockResolvedValue(vi.fn().mockResolvedValue(undefined)),
37+
}))
38+
3539
// Mock fs (createWriteStream and createReadStream for checksum verification)
40+
let closeHandler: (() => void) | undefined
3641
const mockWriteStream = {
3742
on: vi.fn(),
3843
close: vi.fn(),
3944
}
45+
const onWriteStreamEvent = (event: string, callback: () => void) => {
46+
if (event === "finish") setImmediate(callback)
47+
if (event === "close") closeHandler = callback
48+
}
4049
vi.mock("fs", () => ({
4150
createWriteStream: vi.fn(() => mockWriteStream),
4251
createReadStream: vi.fn(() => {
@@ -103,8 +112,9 @@ describe("SEMBLE_SHA256 checksum fixture", () => {
103112
describe("semble-downloader", () => {
104113
beforeEach(() => {
105114
vi.clearAllMocks()
106-
mockWriteStream.on = vi.fn()
107-
mockWriteStream.close = vi.fn()
115+
closeHandler = undefined
116+
mockWriteStream.on = vi.fn(onWriteStreamEvent)
117+
mockWriteStream.close = vi.fn(() => closeHandler?.())
108118

109119
// Restore the default https.get mock so tests that override it don't leak
110120
;(https.get as any).mockImplementation((_url: string, callback: (res: any) => void) => {
@@ -223,13 +233,6 @@ describe("semble-downloader", () => {
223233
// No version file exists
224234
;(fs.readFile as any).mockRejectedValue(new Error("ENOENT"))
225235

226-
// Simulate successful download: pipe is called, then "finish" fires
227-
mockWriteStream.on.mockImplementation((event: string, cb: () => void) => {
228-
if (event === "finish") {
229-
setImmediate(cb)
230-
}
231-
})
232-
233236
try {
234237
const result = await downloadSemble("/storage")
235238

@@ -263,12 +266,14 @@ describe("semble-downloader", () => {
263266
)
264267
// Version file should be written
265268
expect(fs.writeFile).toHaveBeenCalledWith(
266-
path.join("/storage", "semble", ".semble-version"),
269+
path.join("/storage", "semble.new", ".semble-version"),
267270
"v0.4.1",
268271
"utf-8",
269272
)
270273
// Archive should be cleaned up (version-prefixed local cache path)
271-
expect(fs.unlink).toHaveBeenCalledWith(path.join("/storage", "v0.4.1-semble-linux-x64-fast.tar.gz"))
274+
expect(fs.rm).toHaveBeenCalledWith(path.join("/storage", "v0.4.1-semble-linux-x64-fast.tar.gz"), {
275+
force: true,
276+
})
272277
} finally {
273278
if (originalPlatform) Object.defineProperty(process, "platform", originalPlatform)
274279
if (originalArch) Object.defineProperty(process, "arch", originalArch)
@@ -325,7 +330,9 @@ describe("semble-downloader", () => {
325330

326331
try {
327332
await expect(downloadSemble("/storage")).rejects.toThrow("Failed to download semble")
328-
expect(fs.unlink).toHaveBeenCalledWith(path.join("/storage", "v0.4.1-semble-linux-arm64-fast.tar.gz"))
333+
expect(fs.rm).toHaveBeenCalledWith(path.join("/storage", "v0.4.1-semble-linux-arm64-fast.tar.gz"), {
334+
force: true,
335+
})
329336
// Should clean up staging directory, not the original
330337
expect(fs.rm).toHaveBeenCalledWith(path.join("/storage", "semble.new"), {
331338
recursive: true,
@@ -374,12 +381,6 @@ describe("semble-downloader", () => {
374381
})
375382

376383
// Simulate successful download on the second response
377-
mockWriteStream.on.mockImplementation((event: string, cb: () => void) => {
378-
if (event === "finish") {
379-
setImmediate(cb)
380-
}
381-
})
382-
383384
try {
384385
const result = await downloadSemble("/storage")
385386

@@ -542,12 +543,6 @@ describe("semble-downloader", () => {
542543
;(fs.readFile as any).mockRejectedValue(new Error("ENOENT"))
543544

544545
// Simulate successful download
545-
mockWriteStream.on.mockImplementation((event: string, cb: () => void) => {
546-
if (event === "finish") {
547-
setImmediate(cb)
548-
}
549-
})
550-
551546
try {
552547
const result = await downloadSemble("/storage")
553548

@@ -582,14 +577,8 @@ describe("semble-downloader", () => {
582577
;(fs.readFile as any).mockRejectedValue(new Error("ENOENT"))
583578

584579
// Simulate successful download
585-
mockWriteStream.on.mockImplementation((event: string, cb: () => void) => {
586-
if (event === "finish") {
587-
setImmediate(cb)
588-
}
589-
})
590-
591580
// Archive cleanup fails but should not throw (only archive removal after extraction)
592-
;(fs.unlink as any).mockRejectedValue(new Error("unlink cleanup failed"))
581+
;(fs.rm as any).mockRejectedValueOnce(new Error("archive cleanup failed"))
593582

594583
try {
595584
const result = await downloadSemble("/storage")
@@ -617,12 +606,6 @@ describe("semble-downloader", () => {
617606
;(fs.access as any).mockResolvedValue(undefined)
618607

619608
// Simulate successful download
620-
mockWriteStream.on.mockImplementation((event: string, cb: () => void) => {
621-
if (event === "finish") {
622-
setImmediate(cb)
623-
}
624-
})
625-
626609
try {
627610
const result = await downloadSemble("/storage")
628611

@@ -641,7 +624,7 @@ describe("semble-downloader", () => {
641624
expect(https.get).toHaveBeenCalledWith(expect.stringContaining("v0.4.1"), expect.any(Function))
642625
// Should write the new version file
643626
expect(fs.writeFile).toHaveBeenCalledWith(
644-
path.join("/storage", "semble", ".semble-version"),
627+
path.join("/storage", "semble.new", ".semble-version"),
645628
"v0.4.1",
646629
"utf-8",
647630
)
@@ -673,12 +656,6 @@ describe("semble-downloader", () => {
673656
;(fs.access as any).mockResolvedValue(undefined)
674657

675658
// Simulate successful download
676-
mockWriteStream.on.mockImplementation((event: string, cb: () => void) => {
677-
if (event === "finish") {
678-
setImmediate(cb)
679-
}
680-
})
681-
682659
try {
683660
const result = await downloadSemble("/storage")
684661

@@ -700,19 +677,23 @@ describe("semble-downloader", () => {
700677
)
701678
// The stale archive is removed before the fresh download to guarantee
702679
// a clean package is verified against the new checksum.
703-
expect(fs.unlink).toHaveBeenCalledWith(versionedArchive)
680+
expect(fs.rm).toHaveBeenCalledWith(versionedArchive, { force: true })
704681
// The prior-version archive (v0.4.0-*) is swept by cleanupStaleArchives
705682
// after a successful install, so a version upgrade doesn't accumulate
706683
// orphaned packages on disk.
707-
expect(fs.unlink).toHaveBeenCalledWith(path.join("/storage", "v0.4.0-semble-linux-x64-fast.tar.gz"))
684+
expect(fs.rm).toHaveBeenCalledWith(path.join("/storage", "v0.4.0-semble-linux-x64-fast.tar.gz"), {
685+
force: true,
686+
})
708687
// The legacy unversioned archive (pre-v0.4.0 cache layout) is also
709688
// swept, covering the v0.3.1 → v0.4.1 upgrade path.
710-
expect(fs.unlink).toHaveBeenCalledWith(path.join("/storage", "semble-linux-x64-fast.tar.gz"))
689+
expect(fs.rm).toHaveBeenCalledWith(path.join("/storage", "semble-linux-x64-fast.tar.gz"), {
690+
force: true,
691+
})
711692
// Unrelated files in the storage dir must not be touched.
712-
expect(fs.unlink).not.toHaveBeenCalledWith(path.join("/storage", "unrelated-file.txt"))
693+
expect(fs.rm).not.toHaveBeenCalledWith(path.join("/storage", "unrelated-file.txt"), expect.anything())
713694
// The new version file is recorded
714695
expect(fs.writeFile).toHaveBeenCalledWith(
715-
path.join("/storage", "semble", ".semble-version"),
696+
path.join("/storage", "semble.new", ".semble-version"),
716697
"v0.4.1",
717698
"utf-8",
718699
)
@@ -769,12 +750,6 @@ describe("semble-downloader", () => {
769750
})
770751

771752
// Simulate successful download
772-
mockWriteStream.on.mockImplementation((event: string, cb: () => void) => {
773-
if (event === "finish") {
774-
setImmediate(cb)
775-
}
776-
})
777-
778753
try {
779754
const result = await downloadSemble("/storage")
780755

@@ -789,7 +764,7 @@ describe("semble-downloader", () => {
789764
)
790765
// Should write version file again
791766
expect(fs.writeFile).toHaveBeenCalledWith(
792-
path.join("/storage", "semble", ".semble-version"),
767+
path.join("/storage", "semble.new", ".semble-version"),
793768
"v0.4.1",
794769
"utf-8",
795770
)
@@ -812,12 +787,6 @@ describe("semble-downloader", () => {
812787
;(fs.access as any).mockResolvedValue(undefined)
813788

814789
// Simulate successful download
815-
mockWriteStream.on.mockImplementation((event: string, cb: () => void) => {
816-
if (event === "finish") {
817-
setImmediate(cb)
818-
}
819-
})
820-
821790
try {
822791
const result = await downloadSemble("/storage")
823792

@@ -831,7 +800,7 @@ describe("semble-downloader", () => {
831800
)
832801
// Should write version file
833802
expect(fs.writeFile).toHaveBeenCalledWith(
834-
path.join("/storage", "semble", ".semble-version"),
803+
path.join("/storage", "semble.new", ".semble-version"),
835804
"v0.4.1",
836805
"utf-8",
837806
)
@@ -856,12 +825,6 @@ describe("semble-downloader", () => {
856825
// readdir rejects — exercises the catch block in cleanupStaleArchives
857826
;(fs.readdir as any).mockRejectedValue(new Error("EACCES"))
858827

859-
mockWriteStream.on.mockImplementation((event: string, cb: () => void) => {
860-
if (event === "finish") {
861-
setImmediate(cb)
862-
}
863-
})
864-
865828
try {
866829
const result = await downloadSemble("/storage")
867830

@@ -892,29 +855,27 @@ describe("semble-downloader", () => {
892855
"unrelated.txt",
893856
])
894857

895-
mockWriteStream.on.mockImplementation((event: string, cb: () => void) => {
896-
if (event === "finish") {
897-
setImmediate(cb)
898-
}
899-
})
900-
901858
try {
902859
await downloadSemble("/storage")
903860

904861
const currentArchive = path.join("/storage", "v0.4.1-semble-linux-x64-fast.tar.gz")
905862
// Stale versioned + legacy unversioned archives are swept
906-
expect(fs.unlink).toHaveBeenCalledWith(path.join("/storage", "v0.4.0-semble-linux-x64-fast.tar.gz"))
907-
expect(fs.unlink).toHaveBeenCalledWith(path.join("/storage", "semble-linux-x64-fast.tar.gz"))
863+
expect(fs.rm).toHaveBeenCalledWith(path.join("/storage", "v0.4.0-semble-linux-x64-fast.tar.gz"), {
864+
force: true,
865+
})
866+
expect(fs.rm).toHaveBeenCalledWith(path.join("/storage", "semble-linux-x64-fast.tar.gz"), {
867+
force: true,
868+
})
908869
// The current archive is never swept by cleanupStaleArchives (it is
909870
// excluded by the currentArchivePath guard). It is unlinked only by
910871
// the pre-download partial-archive cleanup and the post-install
911872
// archive cleanup steps. unrelated.txt is never touched.
912-
expect(fs.unlink).not.toHaveBeenCalledWith(path.join("/storage", "unrelated.txt"))
873+
expect(fs.rm).not.toHaveBeenCalledWith(path.join("/storage", "unrelated.txt"), expect.anything())
913874
// Sanity: the current archive path is never passed to the stale sweep.
914875
// It is unlinked exactly twice (pre-download cleanup + post-install
915876
// archive cleanup), never via cleanupStaleArchives.
916-
const currentUnlinks = (fs.unlink as any).mock.calls.filter((c: any[]) => c[0] === currentArchive)
917-
expect(currentUnlinks.length).toBe(2)
877+
const currentRemovals = (fs.rm as any).mock.calls.filter((c: any[]) => c[0] === currentArchive)
878+
expect(currentRemovals.length).toBe(2)
918879
} finally {
919880
if (originalPlatform) Object.defineProperty(process, "platform", originalPlatform)
920881
if (originalArch) Object.defineProperty(process, "arch", originalArch)

0 commit comments

Comments
 (0)