From c17ea96bcea405b6736e256c111c0e0898d41715 Mon Sep 17 00:00:00 2001 From: Monalisa Biswal Date: Mon, 5 Oct 2026 22:51:31 -0400 Subject: [PATCH 1/5] Restore publicly available dev dependencies Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b5a57f24-f683-48de-abce-24cc5bc03e96 --- package-lock.json | 26 +++++++++++++------------- package.json | 4 ++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index d9c4e51..7369470 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,9 @@ }, "devDependencies": { "@playwright/test": "^1.63.0", - "@types/node": "^26.6.4", + "@types/node": "^22.18.3", "eslint": "^9.35.0", - "globals": "^17.13.0", + "globals": "^16.3.0", "typescript": "^7.0.2" }, "engines": { @@ -274,13 +274,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.6.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz", - "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==", + "version": "22.18.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/node/-/node-22.18.3.tgz", + "integrity": "sha1-4aTraApogUHN02nFoDX0jPjs6eM=", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~8.9.0" + "undici-types": "~6.21.0" } }, "node_modules/@typescript/typescript-aix-ppc64": { @@ -813,7 +813,7 @@ }, "node_modules/eslint": { "version": "9.35.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/eslint/-/eslint-9.35.0.tgz", "integrity": "sha1-eokFS3ue4d/RtiA12M51VHdz9H4=", "dev": true, "license": "MIT", @@ -1052,9 +1052,9 @@ } }, "node_modules/globals": { - "version": "17.13.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.13.0.tgz", - "integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==", + "version": "16.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/globals/-/globals-16.3.0.tgz", + "integrity": "sha1-ZhGOdl3a+eLYgPfhdlhUP5Px9mc=", "dev": true, "license": "MIT", "engines": { @@ -1499,9 +1499,9 @@ } }, "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "version": "6.21.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha1-aR0ArzkJvpOn+qE75hs6W1DvEss=", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 3e90039..58e288d 100644 --- a/package.json +++ b/package.json @@ -72,9 +72,9 @@ ], "devDependencies": { "@playwright/test": "^1.63.0", - "@types/node": "^26.6.4", + "@types/node": "^22.18.3", "eslint": "^9.35.0", - "globals": "^17.13.0", + "globals": "^16.3.0", "typescript": "^7.0.2" } } From 2f8039b4f7ba0e73ea8b92d97ff3e7a01be070eb Mon Sep 17 00:00:00 2001 From: Monalisa Biswal Date: Sun, 11 Oct 2026 20:28:29 -0400 Subject: [PATCH 2/5] Grant non-interactive agents access to linked node_modules directories Dashboard and run executors now pass --add-dir for node_modules junctions that resolve outside the repository, so auto-denied path prompts no longer block jobs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d8b7888d-c8b2-4e87-95a4-623577cf1e57 --- bin/fixlab.js | 7 +++-- dashboard/server.js | 48 ++++++++++++++++++++++++++++++++--- test/dashboard-server.test.js | 28 ++++++++++++++++++++ 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/bin/fixlab.js b/bin/fixlab.js index f728ed9..12f9687 100644 --- a/bin/fixlab.js +++ b/bin/fixlab.js @@ -21,6 +21,7 @@ import { DEFAULT_DASHBOARD_PORT, FIXLAB_RUNTIMES, inspectRepository, + linkedDependencyDirectories, validateTargetEnvironment, validateLiveTestProfile, validatePort @@ -942,12 +943,14 @@ function launch(repository, request, runtime, targetEnvironment = "") { ? buildCopilotInvocation({ packageRoot, prompt, - sessionId: randomUUID() + sessionId: randomUUID(), + additionalDirs: linkedDependencyDirectories(repository) }) : buildAgencyInvocation({ packageRoot, prompt, - sessionId: randomUUID() + sessionId: randomUUID(), + additionalDirs: linkedDependencyDirectories(repository) }) : { args: [ diff --git a/dashboard/server.js b/dashboard/server.js index 7c3e1a7..c2a5f74 100644 --- a/dashboard/server.js +++ b/dashboard/server.js @@ -3,6 +3,7 @@ import { createHash, randomUUID } from "node:crypto"; import { createReadStream, existsSync, + lstatSync, mkdirSync, readdirSync, readFileSync, @@ -1341,11 +1342,48 @@ function writeCacheSummary(context, job) { } } +// Linked dependency folders (for example a node_modules junction) resolve outside +// the repository, and non-interactive agents cannot answer the path prompt they raise. +export function linkedDependencyDirectories(repository) { + const root = realpathSync(repository); + const found = new Set(); + const inspect = (directory) => { + const candidate = join(directory, "node_modules"); + try { + if (!lstatSync(candidate).isSymbolicLink()) { + return; + } + const target = realpathSync(candidate); + const relation = relative(root, target); + if (relation.startsWith("..") || isAbsolute(relation)) { + found.add(target); + } + } catch { + // Missing or unreadable dependency folders need no extra access. + } + }; + inspect(root); + try { + for (const entry of readdirSync(root, { withFileTypes: true })) { + if (entry.isDirectory() && !entry.name.startsWith(".")) { + inspect(join(root, entry.name)); + } + } + } catch { + // Directory listing is best effort. + } + return [...found]; +} + +const additionalDirectoryArguments = (additionalDirs) => + additionalDirs.flatMap((directory) => ["--add-dir", directory]); + export function buildAgencyInvocation({ packageRoot, prompt, sessionId, - resume = false + resume = false, + additionalDirs = [] }) { if (!sessionId) { throw new Error("Agency invocation requires a session ID"); @@ -1358,6 +1396,7 @@ export function buildAgencyInvocation({ "--agent", "fixlab:fixlab", "--allow-all-tools", + ...additionalDirectoryArguments(additionalDirs), "--no-ask-user", "--stream", "on", @@ -1371,7 +1410,8 @@ export function buildCopilotInvocation({ packageRoot, prompt, sessionId, - resume = false + resume = false, + additionalDirs = [] }) { if (!sessionId) { throw new Error("Copilot invocation requires a session ID"); @@ -1383,6 +1423,7 @@ export function buildCopilotInvocation({ "--agent", "fixlab:fixlab", "--allow-all-tools", + ...additionalDirectoryArguments(additionalDirs), "--no-ask-user", "--autopilot", "--stream", @@ -1420,7 +1461,8 @@ function createExecutor({ command, packageRoot, buildInvocation }) { packageRoot, prompt, sessionId, - resume + resume, + additionalDirs: linkedDependencyDirectories(repository) }); const child = spawn(executable, invocation.args, { cwd: repository, diff --git a/test/dashboard-server.test.js b/test/dashboard-server.test.js index 25302b6..b73fcc1 100644 --- a/test/dashboard-server.test.js +++ b/test/dashboard-server.test.js @@ -6,7 +6,9 @@ import { mkdirSync, mkdtempSync, readFileSync, + realpathSync, rmSync, + symlinkSync, writeFileSync } from "node:fs"; import { createServer } from "node:net"; @@ -27,6 +29,7 @@ import { createProcessCompletion, FIXLAB_STAGES, inspectRepository, + linkedDependencyDirectories, MAX_JOB_ACTIVITY_ENTRIES, MAX_CACHE_BYTES, MAX_CACHE_ENTRIES, @@ -39,6 +42,31 @@ const packageRoot = new URL("..", import.meta.url).pathname.replace( "$1" ); +test("non-interactive executors are granted linked node_modules directories", () => { + const base = mkdtempSync(join(tmpdir(), "fixlab-linked-")); + try { + const repository = join(base, "repo"); + const external = join(base, "external", "node_modules"); + mkdirSync(join(repository, "app"), { recursive: true }); + mkdirSync(external, { recursive: true }); + symlinkSync(external, join(repository, "app", "node_modules"), "junction"); + + const directories = linkedDependencyDirectories(repository); + assert.deepEqual(directories, [realpathSync(external)]); + + const invocation = buildCopilotInvocation({ + packageRoot: "C:\\FixLab", + prompt: "x", + sessionId: "11111111-1111-4111-8111-111111111111", + additionalDirs: directories + }); + const index = invocation.args.indexOf("--add-dir"); + assert.equal(invocation.args[index + 1], realpathSync(external)); + } finally { + rmSync(base, { recursive: true, force: true }); + } +}); + test("Agency executor keeps long prompts out of process arguments", () => { const prompt = `Fix this batch:\n${"x".repeat(40000)}`; const invocation = buildAgencyInvocation({ From e06f6de59c5a7acbd9f34871e2f3b66906f048ba Mon Sep 17 00:00:00 2001 From: Monalisa Biswal Date: Sun, 11 Oct 2026 20:37:53 -0400 Subject: [PATCH 3/5] Allow dismissing a blocked dashboard job to continue the queue A blocked job previously held the queue until resumed; only failed jobs could be dismissed. Blocked jobs stay blocked in history. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d8b7888d-c8b2-4e87-95a4-623577cf1e57 --- dashboard/public/app.js | 12 +++++--- dashboard/server.js | 7 ++--- test/dashboard-server.test.js | 52 +++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/dashboard/public/app.js b/dashboard/public/app.js index 300a70c..87a493d 100644 --- a/dashboard/public/app.js +++ b/dashboard/public/app.js @@ -442,7 +442,11 @@ function renderJob(job, currentActiveJob = job) { canResume && job?.pullRequestReadiness?.status === "approval-required"; const canDismissFailedJob = - canResume && job?.status === "failed"; + canResume && ["failed", "blocked"].includes(job?.status); + dismissFailedJobButton.textContent = + job?.status === "blocked" + ? "Dismiss blocked job and continue queue" + : "Dismiss failed job and continue queue"; const canCancelQueuedJob = !isActive && job?.status === "queued"; jobInputPanel.hidden = !(canResume || canComment || canCancelQueuedJob); @@ -1223,7 +1227,7 @@ dismissFailedJobButton.addEventListener("click", async () => { formError.textContent = ""; if ( !window.confirm( - "Dismiss this failed job? It will remain failed in dashboard history, its session will not resume, and the next queued job will start." + `Dismiss this ${activeJob?.status === "blocked" ? "blocked" : "failed"} job? It will remain ${activeJob?.status === "blocked" ? "blocked" : "failed"} in dashboard history, its session will not resume, and the next queued job will start.` ) ) { return; @@ -1234,8 +1238,8 @@ dismissFailedJobButton.addEventListener("click", async () => { method: "POST" }); jobInputMessage.textContent = body.job - ? "Failed job dismissed. The next queued job has started." - : "Failed job dismissed. The queue is empty."; + ? "Job dismissed. The next queued job has started." + : "Job dismissed. The queue is empty."; if (body.dismissedJob) { rememberJob(body.dismissedJob); } diff --git a/dashboard/server.js b/dashboard/server.js index c2a5f74..d312388 100644 --- a/dashboard/server.js +++ b/dashboard/server.js @@ -2783,9 +2783,9 @@ export function createDashboardServer({ }); return; } - if (currentJob.status !== "failed") { + if (!["failed", "blocked"].includes(currentJob.status)) { sendJson(response, 409, { - error: "only a failed FixLab job can be dismissed" + error: "only a failed or blocked FixLab job can be dismissed" }); return; } @@ -2795,8 +2795,7 @@ export function createDashboardServer({ index: dismissedJob.nextLogIndex, timestamp: new Date().toISOString(), stream: "dashboard", - message: - "Failed job dismissed by the user; its failed outcome remains in dashboard history." + message: `${dismissedJob.status === "blocked" ? "Blocked" : "Failed"} job dismissed by the user; its ${dismissedJob.status} outcome remains in dashboard history.` }); if (dismissedJob.artifactDirectory) { removeArtifactDirectory(dismissedJob.artifactDirectory); diff --git a/test/dashboard-server.test.js b/test/dashboard-server.test.js index b73fcc1..4922694 100644 --- a/test/dashboard-server.test.js +++ b/test/dashboard-server.test.js @@ -1944,6 +1944,58 @@ test("dashboard persists privacy-safe token and duration metrics", async () => { rmSync(repository, { recursive: true, force: true }); }); +test("dashboard dismisses a blocked job and continues the queue", async () => { + const repository = createGitRepository(); + let executorCalls = 0; + const executor = ({ onOutput }) => { + executorCalls += 1; + if (executorCalls === 1) { + for (const stage of FIXLAB_STAGES.slice(0, -1)) { + onOutput("stdout", `FIXLAB_STAGE|${stage}|passed|completed\n`); + } + onOutput( + "stdout", + "FIXLAB_STAGE|pr|blocked|Approval is required before creating the pull request.\n" + ); + return { completion: Promise.resolve({ code: 0 }), terminate() {} }; + } + return { completion: new Promise(() => {}), terminate() {} }; + }; + const { dashboard, url } = await startDashboard(repository, executor); + + try { + const post = (request) => + jsonRequest(url, "/api/jobs", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + request, + requestType: "bug-fix", + mode: "validate-only" + }) + }); + await post("This job ends blocked."); + await new Promise((resolve) => setTimeout(resolve, 0)); + assert.equal((await jsonRequest(url, "/api/job")).body.job.status, "blocked"); + + const queued = await post("This job waits behind the blocked one."); + assert.equal(queued.body.queued, true); + + const dismissed = await jsonRequest(url, "/api/job/dismiss", { + method: "POST" + }); + assert.equal(dismissed.response.status, 202, JSON.stringify(dismissed.body)); + assert.equal(dismissed.body.dismissedJob.status, "blocked"); + assert.equal(dismissed.body.job.request, "This job waits behind the blocked one."); + assert.equal(dismissed.body.job.status, "running"); + assert.equal(dismissed.body.history[0].status, "blocked"); + assert.equal(executorCalls, 2); + } finally { + await dashboard.close(); + rmSync(repository, { recursive: true, force: true }); + } +}); + test("dashboard restores private job summaries and PR readiness after restart", async () => { const repository = createGitRepository(); const executor = ({ onOutput }) => { From 83c6ade4dd1cc5fa1b36da27cc3093460c653bd8 Mon Sep 17 00:00:00 2001 From: Monalisa Biswal Date: Sun, 11 Oct 2026 20:49:03 -0400 Subject: [PATCH 4/5] Restrict linked node_modules access to profile-trusted roots Address review: a repository-controlled node_modules symlink no longer grants --add-dir on its own. Targets must fall under trustedDependencyRoots declared in the repository profile (filesystem roots ignored), and the profile's configured working directories are searched instead of only one directory level. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d8b7888d-c8b2-4e87-95a4-623577cf1e57 --- dashboard/server.js | 78 ++++++++++++++++++++++++++++------- docs/repository-onboarding.md | 6 +++ test/dashboard-server.test.js | 39 +++++++++++++++--- 3 files changed, 103 insertions(+), 20 deletions(-) diff --git a/dashboard/server.js b/dashboard/server.js index d312388..b1df376 100644 --- a/dashboard/server.js +++ b/dashboard/server.js @@ -19,6 +19,7 @@ import { extname, isAbsolute, join, + parse, relative, resolve, sep @@ -1344,33 +1345,80 @@ function writeCacheSummary(context, job) { // Linked dependency folders (for example a node_modules junction) resolve outside // the repository, and non-interactive agents cannot answer the path prompt they raise. +// Only targets inside a profile-declared trustedDependencyRoots entry are granted, so a +// repository-controlled symlink alone never widens the agent's file access. export function linkedDependencyDirectories(repository) { - const root = realpathSync(repository); + let root; + let profile; + try { + root = realpathSync(repository); + profile = loadRepositoryProfile(root); + } catch { + return []; + } + + const trustedRoots = ( + Array.isArray(profile?.trustedDependencyRoots) + ? profile.trustedDependencyRoots + : [] + ) + .filter((value) => typeof value === "string" && isAbsolute(value)) + .flatMap((value) => { + try { + const resolved = realpathSync(value); + return resolved === parse(resolved).root ? [] : [resolved]; + } catch { + return []; + } + }); + if (trustedRoots.length === 0) { + return []; + } + + const isInside = (parent, child) => { + const relation = relative(parent, child); + return ( + relation === "" || + (!relation.startsWith("..") && !isAbsolute(relation)) + ); + }; + const workingDirectories = new Set([root]); + for (const configured of [ + profile.browserAutomation?.workingDirectory, + ...Object.values(profile.applications ?? {}).map( + (application) => application?.workingDirectory + ) + ]) { + if (typeof configured !== "string") { + continue; + } + try { + const directory = realpathSync(resolve(root, configured)); + if (isInside(root, directory)) { + workingDirectories.add(directory); + } + } catch { + // A missing working directory has no dependency link to grant. + } + } + const found = new Set(); - const inspect = (directory) => { + for (const directory of workingDirectories) { const candidate = join(directory, "node_modules"); try { if (!lstatSync(candidate).isSymbolicLink()) { - return; + continue; } const target = realpathSync(candidate); - const relation = relative(root, target); - if (relation.startsWith("..") || isAbsolute(relation)) { + if ( + !isInside(root, target) && + trustedRoots.some((trusted) => isInside(trusted, target)) + ) { found.add(target); } } catch { // Missing or unreadable dependency folders need no extra access. } - }; - inspect(root); - try { - for (const entry of readdirSync(root, { withFileTypes: true })) { - if (entry.isDirectory() && !entry.name.startsWith(".")) { - inspect(join(root, entry.name)); - } - } - } catch { - // Directory listing is best effort. } return [...found]; } diff --git a/docs/repository-onboarding.md b/docs/repository-onboarding.md index 2601b38..eb1a865 100644 --- a/docs/repository-onboarding.md +++ b/docs/repository-onboarding.md @@ -56,6 +56,12 @@ The profile must also declare: - `browserAutomation.dataSafety.policy`, describing mock, read-only, or intercepted-mutation behavior. - `browserAutomation.dataSafety.productionAllowed: false`. +Optionally, declare `trustedDependencyRoots`, a list of absolute directories, when a +working directory's `node_modules` is a link to a shared install outside the +repository. Non-interactive agents cannot answer path-permission prompts, so FixLab +passes `--add-dir` only for `node_modules` links under these roots, in the profile's +working directories. Without this field, no external path is granted. Filesystem roots +are ignored. Synthesized scenario source is transient by default. The agent must remove generated test files and validation-only configuration changes before the product pull request. A repository can require a permanent generated browser diff --git a/test/dashboard-server.test.js b/test/dashboard-server.test.js index 4922694..4c75e86 100644 --- a/test/dashboard-server.test.js +++ b/test/dashboard-server.test.js @@ -13,7 +13,7 @@ import { } from "node:fs"; import { createServer } from "node:net"; import { tmpdir } from "node:os"; -import { dirname, join } from "node:path"; +import { dirname, join, parse } from "node:path"; import { PassThrough, Readable, Writable } from "node:stream"; import test from "node:test"; import { @@ -42,15 +42,44 @@ const packageRoot = new URL("..", import.meta.url).pathname.replace( "$1" ); -test("non-interactive executors are granted linked node_modules directories", () => { +test("linked node_modules directories are granted only inside trusted dependency roots", () => { const base = mkdtempSync(join(tmpdir(), "fixlab-linked-")); try { const repository = join(base, "repo"); - const external = join(base, "external", "node_modules"); - mkdirSync(join(repository, "app"), { recursive: true }); + const trusted = join(base, "trusted"); + const external = join(trusted, "web", "node_modules"); + const untrusted = join(base, "untrusted", "node_modules"); + mkdirSync(join(repository, "apps", "web"), { recursive: true }); + mkdirSync(join(repository, "tools"), { recursive: true }); mkdirSync(external, { recursive: true }); - symlinkSync(external, join(repository, "app", "node_modules"), "junction"); + mkdirSync(untrusted, { recursive: true }); + symlinkSync( + external, + join(repository, "apps", "web", "node_modules"), + "junction" + ); + symlinkSync(untrusted, join(repository, "tools", "node_modules"), "junction"); + + const writeProfile = (trustedDependencyRoots) => { + const directory = join(repository, ".github", "fixlab"); + mkdirSync(directory, { recursive: true }); + const profile = validProfile({ trustedDependencyRoots }); + profile.browserAutomation.workingDirectory = "apps/web"; + profile.applications.frontend.workingDirectory = "apps/web"; + profile.applications.tools = { workingDirectory: "tools" }; + writeFileSync( + join(directory, "repository-profile.json"), + JSON.stringify(profile) + ); + }; + + writeProfile(undefined); + assert.deepEqual(linkedDependencyDirectories(repository), []); + + writeProfile([parse(base).root]); + assert.deepEqual(linkedDependencyDirectories(repository), []); + writeProfile([trusted]); const directories = linkedDependencyDirectories(repository); assert.deepEqual(directories, [realpathSync(external)]); From 6cde49ce234e04bee2a5a4de98c5398bce273538 Mon Sep 17 00:00:00 2001 From: Monalisa Biswal Date: Mon, 12 Oct 2026 13:10:22 -0400 Subject: [PATCH 5/5] Grant agents access to each job's screenshot directory Dashboard-launched agents could not read uploaded screenshots stored in the job artifact directory outside the repository, which blocked diagnosis. Pass only that job's artifact directory via --add-dir. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: aa2b2938-9ca6-4b67-a11a-f131b554d603 --- dashboard/server.js | 15 +++++++++++++-- test/dashboard-server.test.js | 5 ++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/dashboard/server.js b/dashboard/server.js index b1df376..d663a8d 100644 --- a/dashboard/server.js +++ b/dashboard/server.js @@ -1498,7 +1498,14 @@ function findDirectExecutable(command) { } function createExecutor({ command, packageRoot, buildInvocation }) { - return ({ repository, prompt, sessionId, resume, onOutput }) => { + return ({ + repository, + artifactDirectories = [], + prompt, + sessionId, + resume, + onOutput + }) => { const executable = findDirectExecutable(command); if (!executable) { throw new Error( @@ -1510,7 +1517,10 @@ function createExecutor({ command, packageRoot, buildInvocation }) { prompt, sessionId, resume, - additionalDirs: linkedDependencyDirectories(repository) + additionalDirs: [ + ...linkedDependencyDirectories(repository), + ...artifactDirectories + ] }); const child = spawn(executable, invocation.args, { cwd: repository, @@ -2512,6 +2522,7 @@ export function createDashboardServer({ job.lastActivityAt = new Date().toISOString(); handle = executor({ repository: resolvedRepository, + artifactDirectories: job.artifactDirectory ? [job.artifactDirectory] : [], prompt, sessionId: job.id, resume, diff --git a/test/dashboard-server.test.js b/test/dashboard-server.test.js index 4c75e86..cccd013 100644 --- a/test/dashboard-server.test.js +++ b/test/dashboard-server.test.js @@ -586,8 +586,10 @@ test("loads Azure DevOps intake and passes local screenshots without caching the }; let loaderInput; let receivedPrompt; - const executor = ({ prompt, onOutput }) => { + let receivedArtifactDirectories; + const executor = ({ prompt, artifactDirectories, onOutput }) => { receivedPrompt = prompt; + receivedArtifactDirectories = artifactDirectories; onOutput( "stdout", "FIXLAB_BUG|71|external|upstream-system|The upstream system owns the rejected request; no repository change is required.\n" @@ -658,6 +660,7 @@ test("loads Azure DevOps intake and passes local screenshots without caching the screenshotPath = pathLine?.slice(2); assert.ok(screenshotPath); assert.equal(existsSync(screenshotPath), true); + assert.deepEqual(receivedArtifactDirectories, [dirname(screenshotPath)]); assert.match(receivedPrompt, /Loaded Azure DevOps selection/); assert.match( receivedPrompt,