Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .agents/session-log.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Session Log - QuantLab

## 2026-04-10 — Real-Path Desktop Validation and Smoke Semantics (Issue #275)
- **Session Focus**: Restore an honest distinction between fallback desktop smoke and real-path desktop validation.
- **Tasks Completed**:
- Updated `desktop/scripts/smoke.js` to parse an explicit `--mode` and pass the selected smoke mode into Electron.
- Kept fallback smoke on the local-shell path by disabling server boot only in fallback mode.
- Hardened `desktop/main.js` so real-path smoke only succeeds when `research_ui` becomes reachable instead of accepting local-runs fallback.
- Updated GitHub Actions to run the explicit `smoke:real-path` path under the dedicated `desktop-real-path` job name.
- **Key Decisions**:
- `smoke:fallback` remains useful for shell/bootstrap coverage, but its semantics stay intentionally tolerant.
- `smoke:real-path` now means real `research_ui` reachability; local runs no longer count as success for that mode.
- CI naming now reflects the stronger guarantee instead of implying generic smoke coverage.
- **Validation Notes**:
- Validate locally with both `npm run smoke:fallback` and `npm run smoke:real-path` from `desktop/`.
- Confirm workflow syntax and behavior with the desktop smoke commands before opening the PR.

## 2026-03-24 — Canonical Run Machine Contract (Issue #62)
- **Session Focus**: Reduce the remaining contract asymmetry between plain `run` and `sweep` inside canonical `report.json`.
- **Tasks Completed**:
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
run: |
python -m pytest -q

desktop-smoke:
desktop-real-path:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -53,7 +53,7 @@ jobs:
run: |
npm ci

- name: Run desktop smoke
- name: Run desktop real-path smoke
working-directory: desktop
run: |
xvfb-run -a npm run smoke
xvfb-run -a npm run smoke:real-path
34 changes: 29 additions & 5 deletions desktop/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const RESEARCH_UI_HEALTH_PATH = "/api/paper-sessions-health";
const RESEARCH_UI_STARTUP_TIMEOUT_MS = 25000;
const ELECTRON_STATE_ROOT = path.join(DESKTOP_OUTPUTS_ROOT, "electron");
const IS_SMOKE_RUN = process.env.QUANTLAB_DESKTOP_SMOKE === "1";
const SMOKE_MODE = process.env.QUANTLAB_DESKTOP_SMOKE_MODE === "real-path" ? "real-path" : "fallback";
const SMOKE_OUTPUT_PATH = process.env.QUANTLAB_DESKTOP_SMOKE_OUTPUT || "";
const SKIP_RESEARCH_UI_BOOT = process.env.QUANTLAB_DESKTOP_DISABLE_SERVER_BOOT === "1";
let smokeResultPersisted = false;
Expand Down Expand Up @@ -364,6 +365,19 @@ async function readProjectJson(targetPath) {
}
}

async function readJsonFile(targetPath) {
const raw = await fsp.readFile(targetPath, "utf8");
try {
return JSON.parse(raw);
} catch (_error) {
const sanitized = raw
.replace(/\bNaN\b/g, "null")
.replace(/\b-Infinity\b/g, "null")
.replace(/\bInfinity\b/g, "null");
return JSON.parse(sanitized);
}
}

function parseYamlSectionValue(raw, sectionName, keyName) {
const sectionPattern = new RegExp(`^${sectionName}:\\s*\\r?\\n([\\s\\S]*?)(?=^\\S|\\Z)`, "m");
const sectionMatch = String(raw || "").match(sectionPattern);
Expand Down Expand Up @@ -905,16 +919,26 @@ async function runDesktopSmoke() {
await new Promise((resolve) => setTimeout(resolve, 250));
}
try {
const localRuns = await readProjectJson("outputs/runs/runs_index.json");
const localRuns = IS_SMOKE_RUN
? await readJsonFile(path.join(OUTPUTS_ROOT, "runs", "runs_index.json"))
: await readProjectJson("outputs/runs/runs_index.json");
result.localRunsReady = Array.isArray(localRuns?.runs);
} catch (_error) {
result.localRunsReady = false;
}
result.shellReady = result.bridgeReady && (result.serverReady || result.localRunsReady);
result.shellReady = result.bridgeReady && (
SMOKE_MODE === "real-path"
? result.serverReady
: (result.serverReady || result.localRunsReady)
);
if (!result.serverReady) {
result.error = workspaceState.error || (result.localRunsReady
? "research_ui did not become reachable, but the shell loaded via the local runs index."
: "research_ui did not become reachable during smoke run.");
result.error = workspaceState.error || (
SMOKE_MODE === "real-path"
? "research_ui did not become reachable during real-path smoke run."
: (result.localRunsReady
? "research_ui did not become reachable, but the shell loaded via the local runs index."
: "research_ui did not become reachable during smoke run.")
);
}
} catch (error) {
result.error = error.message;
Expand Down
44 changes: 32 additions & 12 deletions desktop/scripts/smoke.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,19 @@ const { spawn } = require("child_process");
/** @typedef {import("../shared/models/smoke").SmokeMode} SmokeMode */
/** @typedef {import("../shared/models/smoke").SmokeResult} SmokeResult */

function parseSmokeMode(argv) {
const rawMode = argv
.map((entry) => String(entry || "").trim())
.find((entry) => entry.startsWith("--mode="))
?.slice("--mode=".length);
if (!rawMode || rawMode === "fallback" || rawMode === "real-path") {
return /** @type {SmokeMode} */ (rawMode || "fallback");
}
throw new Error(`Unsupported desktop smoke mode: ${rawMode}`);
}

async function main() {
const mode = parseSmokeMode(process.argv.slice(2));
const desktopRoot = path.resolve(__dirname, "..");
const projectRoot = path.resolve(desktopRoot, "..");
const electronBinary = require("electron");
Expand All @@ -15,8 +27,8 @@ async function main() {
const outputPath = path.join(tempRoot, "result.json");
const desktopOutputsRoot = path.join(tempRoot, "outputs");
const workspaceStatePath = path.join(desktopOutputsRoot, "desktop", "workspace_state.json");
const projectRunsDir = path.join(projectRoot, "outputs", "runs");
const projectRunsIndexPath = path.join(projectRunsDir, "runs_index.json");
const smokeRunsDir = path.join(desktopOutputsRoot, "runs");
const smokeRunsIndexPath = path.join(smokeRunsDir, "runs_index.json");
let seededRunsIndex = false;

await fs.mkdir(path.dirname(workspaceStatePath), { recursive: true });
Expand Down Expand Up @@ -51,11 +63,11 @@ async function main() {
);

try {
await fs.access(projectRunsIndexPath);
await fs.access(smokeRunsIndexPath);
} catch (_error) {
await fs.mkdir(projectRunsDir, { recursive: true });
await fs.mkdir(smokeRunsDir, { recursive: true });
await fs.writeFile(
projectRunsIndexPath,
smokeRunsIndexPath,
`${JSON.stringify({
updated_at: new Date().toISOString(),
runs: [
Expand Down Expand Up @@ -86,9 +98,10 @@ async function main() {
env: {
...process.env,
QUANTLAB_DESKTOP_SMOKE: "1",
QUANTLAB_DESKTOP_SMOKE_MODE: mode,
QUANTLAB_DESKTOP_SMOKE_OUTPUT: outputPath,
QUANTLAB_DESKTOP_OUTPUTS_ROOT: desktopOutputsRoot,
QUANTLAB_DESKTOP_DISABLE_SERVER_BOOT: "1",
...(mode === "fallback" ? { QUANTLAB_DESKTOP_DISABLE_SERVER_BOOT: "1" } : {}),
},
});
let stdout = "";
Expand Down Expand Up @@ -129,10 +142,15 @@ async function main() {
);
}

if (exitCode !== 0 || !result.bridgeReady || !result.shellReady) {
const passed =
mode === "real-path"
? exitCode === 0 && result.bridgeReady && result.serverReady && result.apiReady
: exitCode === 0 && result.bridgeReady && result.shellReady;

if (!passed) {
if (stdout.trim()) console.error(stdout.trim());
if (stderr.trim()) console.error(stderr.trim());
throw new Error(`Desktop smoke failed: ${JSON.stringify(result)}`);
throw new Error(`Desktop smoke (${mode}) failed: ${JSON.stringify(result)}`);
}

const persistedWorkspaceRaw = await fs.readFile(workspaceStatePath, "utf8");
Expand All @@ -144,13 +162,15 @@ async function main() {
}

console.log(
result.serverReady
? `Desktop smoke passed via ${result.serverUrl}`
: "Desktop smoke passed via local runs fallback",
mode === "real-path"
? `Desktop smoke real-path passed via ${result.serverUrl}`
: (result.serverReady
? `Desktop smoke fallback passed via ${result.serverUrl}`
: "Desktop smoke fallback passed via local runs fallback"),
);
} finally {
if (seededRunsIndex) {
await fs.rm(projectRunsIndexPath, { force: true }).catch(() => {});
await fs.rm(smokeRunsIndexPath, { force: true }).catch(() => {});
}
}
}
Expand Down
Loading