From 90717386a3936c0e3112799103fe0efd63c79143 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Mon, 9 Nov 2026 00:01:30 +0700 Subject: [PATCH 1/2] test(render): read the request before a stand-in worker answers A stand-in that answered and exited before the parent wrote its request made the write fail with a broken pipe, so the frame assertions failed at random in CI. The stand-in now consumes the length-prefixed request first, as a real worker does. Signed-off-by: Jeremi Joslin --- crates/registry-render/src/worker.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/crates/registry-render/src/worker.rs b/crates/registry-render/src/worker.rs index 75b82e6d6b..f74e9f249f 100644 --- a/crates/registry-render/src/worker.rs +++ b/crates/registry-render/src/worker.rs @@ -621,8 +621,13 @@ mod tests { framing_stand_in(dir, &body, after) } - /// A stand-in worker that writes `body` as one frame and then runs - /// `after` (shell). + /// A stand-in worker that reads the whole request frame, writes `body` as + /// one frame and then runs `after` (shell). A real worker reads its request + /// before it answers; one that answers and exits first would make the + /// parent's request write fail with a broken pipe. The parent keeps stdin + /// open until it has the answer, so the request is read by its length + /// prefix (little-endian `u32`), one byte per `dd` read so that nothing + /// past the frame is consumed. fn framing_stand_in(dir: &std::path::Path, body: &[u8], after: &str) -> std::path::PathBuf { let mut frame = (body.len() as u32).to_le_bytes().to_vec(); frame.extend_from_slice(body); @@ -631,7 +636,14 @@ mod tests { let stand_in = dir.join("answering-worker"); std::fs::write( &stand_in, - format!("#!/bin/sh\ncat '{}'\n{after}\n", frame_file.display()), + format!( + "#!/bin/sh\n\ + set -- $(dd bs=1 count=4 2>/dev/null | od -An -tu1)\n\ + dd bs=1 count=$(($1 + $2 * 256 + $3 * 65536 + $4 * 16777216)) \ + of=/dev/null 2>/dev/null\n\ + cat '{}'\n{after}\n", + frame_file.display() + ), ) .expect("write stand-in"); use std::os::unix::fs::PermissionsExt; From 3f78be2b65197aebefad0eb64fe48cfefc834d5d Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Mon, 9 Nov 2026 00:01:30 +0700 Subject: [PATCH 2/2] test(vscode): wait for the file event and folder save the test depends on An edit made outside the editor reaches the language server only through a VS Code watcher event, which Linux runners occasionally drop. The test now rewrites the file until the extension host reports it, so a missing symbol means a delivered change was not indexed. A workspace configuration update also waits for the folder change to be saved, since VS Code refuses it while the workspace file is dirty. Signed-off-by: Jeremi Joslin --- editors/vscode/test/trusted.test.ts | 39 +++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/editors/vscode/test/trusted.test.ts b/editors/vscode/test/trusted.test.ts index 043633af97..23c678faa9 100644 --- a/editors/vscode/test/trusted.test.ts +++ b/editors/vscode/test/trusted.test.ts @@ -140,7 +140,7 @@ suite('Registry Stack extension', () => { const alphaFolder = vscode.workspace.workspaceFolders?.find((folder) => folder.name === 'alpha'); assert.ok(alphaFolder, 'alpha workspace folder is available'); - fs.writeFileSync( + await writeWatchedFile( path.join(alphaFolder.uri.fsPath, 'registry.yaml'), 'apiVersion: id.registrystack.org/formats/breg/project/v1alpha1\nkind: BRegProject\nproject: { id: alpha-reloaded }\n', ); @@ -271,7 +271,7 @@ suite('Registry Stack extension', () => { await vscode.workspace.openTextDocument(retiredQuestionPath); await assertLanguageServerStartCountStable(startLog, 4); - assert.strictEqual(vscode.workspace.updateWorkspaceFolders(3, 3), true); + await changeWorkspaceFolders(3, 3); await assertWorkspaceFolderCount(3); // Deleting the installer metadata and restarting proves the PATH @@ -335,6 +335,9 @@ async function assertWorkspaceFolderCount(expected: number): Promise { // VS Code requires the workspace-change event to finish before another // updateWorkspaceFolders call, even when workspaceFolders already has its // next value. Rapid product add/remove cycles must await that event. +// A workspace-scoped configuration update must also wait: it edits the same +// .code-workspace file, and VS Code refuses it while the folder change is +// still unsaved there. async function changeWorkspaceFolders( start: number, deleteCount: number, @@ -352,6 +355,38 @@ async function changeWorkspaceFolders( } } +// An edit made outside the editor reaches the language server only as a file +// event from VS Code's watcher, which does not guarantee an event for every +// write. Rewrite the file until the extension host reports it, so a later +// symbol assertion fails only when a delivered change was not indexed. A +// string glob adds no watch request of its own and sees the same events as the +// language client's watchers. +async function writeWatchedFile(filePath: string, content: string): Promise { + const watcher = vscode.workspace.createFileSystemWatcher(`**/${path.basename(filePath)}`); + let reported = false; + const onEvent = (uri: vscode.Uri) => { + if (uri.fsPath === filePath) { + reported = true; + } + }; + watcher.onDidCreate(onEvent); + watcher.onDidChange(onEvent); + try { + for (let write = 0; write < 3; write += 1) { + fs.writeFileSync(filePath, content); + for (let attempt = 0; attempt < 20; attempt += 1) { + if (reported) { + return; + } + await new Promise((resolve) => setTimeout(resolve, 100)); + } + } + assert.fail(`VS Code reported no file event for ${filePath}`); + } finally { + watcher.dispose(); + } +} + async function assertWorkspaceSymbol(expected: string, documentPath?: string): Promise { const expectedPath = documentPath === undefined ? undefined : fs.realpathSync(documentPath); for (let attempt = 0; attempt < 50; attempt += 1) {