Skip to content

Commit 554d91e

Browse files
committed
fix(e2e): clean up scenario workspace after artifact publication fails
1 parent b5396be commit 554d91e

3 files changed

Lines changed: 66 additions & 41 deletions

File tree

‎apps/vscode-e2e/src/restart/scenarioWorkspace.test.ts‎

Lines changed: 54 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import fs from "node:fs/promises"
33
import path from "node:path"
44
import test, { type TestContext } from "node:test"
55

6-
import { createScenarioWorkspace, preserveScenarioWorkspace, removeScenarioWorkspace } from "./scenarioWorkspace"
6+
import {
7+
createScenarioWorkspace,
8+
preserveScenarioWorkspace,
9+
preserveScenarioWorkspaceForRunner,
10+
removeScenarioWorkspace,
11+
} from "./scenarioWorkspace"
712

813
async function scenario(t: TestContext) {
914
const workspace = await createScenarioWorkspace()
@@ -127,45 +132,55 @@ void test("records collection failures without copying raw files and still clean
127132
await assert.rejects(fs.access(workspace.root), { code: "ENOENT" })
128133
})
129134

130-
for (const failure of ["partial write", "rename"] as const) {
131-
for (const existing of [false, true]) {
132-
void test(`keeps the source after a failed ${failure}, previous artifact: ${existing}`, async (t) => {
133-
const { workspace, destination } = await scenario(t)
134-
await fs.mkdir(destination, { recursive: true })
135-
const artifact = path.join(destination, "diagnostics.json")
136-
const previous = JSON.stringify({ previous: true })
137-
if (existing) await fs.writeFile(artifact, previous)
138-
const write = fs.writeFile
139-
if (failure === "partial write") {
140-
t.mock.method(fs, "writeFile", async (file: Parameters<typeof fs.writeFile>[0]) => {
141-
await write(file, '{"partial":')
142-
throw Object.assign(new Error("disk failed"), { code: "EIO" })
143-
})
144-
} else {
145-
t.mock.method(fs, "rename", async () => {
146-
throw Object.assign(new Error("rename failed"), { code: "EIO" })
147-
})
148-
}
149-
await assert.rejects(preserveScenarioWorkspace(workspace), { code: "EIO" })
150-
await fs.access(workspace.root)
151-
if (existing) assert.equal(await fs.readFile(artifact, "utf8"), previous)
152-
else await assert.rejects(fs.access(artifact), { code: "ENOENT" })
153-
assert.deepEqual(await fs.readdir(destination), existing ? ["diagnostics.json"] : [])
154-
})
135+
const preservationEntries = [
136+
{ name: "retryable preservation", preserve: preserveScenarioWorkspace, keepsSource: true },
137+
{ name: "runner cleanup", preserve: preserveScenarioWorkspaceForRunner, keepsSource: false },
138+
]
139+
140+
for (const entry of preservationEntries) {
141+
for (const failure of ["partial write", "rename"] as const) {
142+
for (const existing of [false, true]) {
143+
void test(`${entry.name}: failed ${failure}, previous artifact: ${existing}`, async (t) => {
144+
const { workspace, destination } = await scenario(t)
145+
await fs.mkdir(destination, { recursive: true })
146+
const artifact = path.join(destination, "diagnostics.json")
147+
const previous = JSON.stringify({ previous: true })
148+
if (existing) await fs.writeFile(artifact, previous)
149+
const write = fs.writeFile
150+
if (failure === "partial write") {
151+
t.mock.method(fs, "writeFile", async (file: Parameters<typeof fs.writeFile>[0]) => {
152+
await write(file, '{"partial":')
153+
throw Object.assign(new Error("disk failed"), { code: "EIO" })
154+
})
155+
} else {
156+
t.mock.method(fs, "rename", async () => {
157+
throw Object.assign(new Error("rename failed"), { code: "EIO" })
158+
})
159+
}
160+
await assert.rejects(entry.preserve(workspace), { code: "EIO" })
161+
if (entry.keepsSource) await fs.access(workspace.root)
162+
else await assert.rejects(fs.access(workspace.root), { code: "ENOENT" })
163+
if (existing) assert.equal(await fs.readFile(artifact, "utf8"), previous)
164+
else await assert.rejects(fs.access(artifact), { code: "ENOENT" })
165+
assert.deepEqual(await fs.readdir(destination), existing ? ["diagnostics.json"] : [])
166+
})
167+
}
155168
}
156169
}
157170

158-
void test("publishes complete diagnostics before removing the source workspace", async (t) => {
159-
const { workspace, destination } = await scenario(t)
160-
const rename = fs.rename
161-
t.mock.method(fs, "rename", async (source: string, target: string) => {
162-
assert.equal(target, path.join(destination, "diagnostics.json"))
163-
await fs.access(workspace.root)
164-
await assert.rejects(fs.access(target), { code: "ENOENT" })
165-
assert(JSON.parse(await fs.readFile(source, "utf8")).sources)
166-
await rename(source, target)
171+
for (const entry of preservationEntries) {
172+
void test(`${entry.name}: publishes complete diagnostics before removing the source workspace`, async (t) => {
173+
const { workspace, destination } = await scenario(t)
174+
const rename = fs.rename
175+
t.mock.method(fs, "rename", async (source: string, target: string) => {
176+
assert.equal(target, path.join(destination, "diagnostics.json"))
177+
await fs.access(workspace.root)
178+
await assert.rejects(fs.access(target), { code: "ENOENT" })
179+
assert(JSON.parse(await fs.readFile(source, "utf8")).sources)
180+
await rename(source, target)
181+
})
182+
await entry.preserve(workspace)
183+
assert.deepEqual(await fs.readdir(destination), ["diagnostics.json"])
184+
await assert.rejects(fs.access(workspace.root), { code: "ENOENT" })
167185
})
168-
await preserveScenarioWorkspace(workspace)
169-
assert.deepEqual(await fs.readdir(destination), ["diagnostics.json"])
170-
await assert.rejects(fs.access(workspace.root), { code: "ENOENT" })
171-
})
186+
}

‎apps/vscode-e2e/src/restart/scenarioWorkspace.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,13 @@ export async function preserveScenarioWorkspace(scenarioWorkspace: ScenarioWorks
248248
await removeScenarioWorkspace(scenarioWorkspace)
249249
return destinationRoot
250250
}
251+
252+
/** The runner has no retry step, so it must also clean up failed publications. */
253+
export async function preserveScenarioWorkspaceForRunner(scenarioWorkspace: ScenarioWorkspace): Promise<string> {
254+
try {
255+
return await preserveScenarioWorkspace(scenarioWorkspace)
256+
} catch (error) {
257+
await removeScenarioWorkspace(scenarioWorkspace)
258+
throw error
259+
}
260+
}

‎apps/vscode-e2e/src/runTest.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { addUseMcpToolResultFixtures } from "./fixtures/use-mcp-tool"
2323
import { addWriteToFileResultFixtures } from "./fixtures/write-to-file"
2424
import {
2525
createScenarioWorkspace,
26-
preserveScenarioWorkspace,
26+
preserveScenarioWorkspaceForRunner,
2727
removeScenarioWorkspace,
2828
} from "./restart/scenarioWorkspace"
2929
import { runRestartScenario } from "./restart/vscodeCoordinator"
@@ -235,7 +235,7 @@ async function main() {
235235
// Raw histories, phase errors, and logs must never enter the artifact.
236236
if (runFailed) {
237237
try {
238-
const preservedAt = await preserveScenarioWorkspace(scenarioWorkspace)
238+
const preservedAt = await preserveScenarioWorkspaceForRunner(scenarioWorkspace)
239239
console.error(`Restart scenario diagnostics saved for failure inspection: ${preservedAt}`)
240240
} catch (preserveError) {
241241
console.error("Failed to preserve the restart scenario workspace:", preserveError)

0 commit comments

Comments
 (0)