Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9b74f98
fix(storage): enforce sole-daemon database ownership
ScriptedAlchemy Jul 14, 2026
6a10719
test(storage): close hook stdin in integrity suite
ScriptedAlchemy Jul 14, 2026
05842e0
fix(storage): finish sole-daemon ownership integration
ScriptedAlchemy Jul 14, 2026
83cc093
Merge remote-tracking branch 'origin/master' into codex/multi-connect…
ScriptedAlchemy Jul 14, 2026
7fd49c1
fix(storage): close sole-daemon integrity gaps
ScriptedAlchemy Jul 14, 2026
2a8831a
fix(storage): repair integrity follow-up build
ScriptedAlchemy Jul 14, 2026
4b65e9b
fix(storage): clear integrity clippy blockers
ScriptedAlchemy Jul 14, 2026
0dcc4ea
fix(hermes): pass explicit stock project scope
ScriptedAlchemy Jul 14, 2026
a1cbb01
fix(hermes): request JSON for stock LCM checks
ScriptedAlchemy Jul 14, 2026
1c210f6
fix(hermes): decode stock LCM contracts
ScriptedAlchemy Jul 14, 2026
ae0f271
fix(hermes): route stock memory providers
ScriptedAlchemy Jul 14, 2026
07a8cad
fix(hermes): request structured memory output
ScriptedAlchemy Jul 14, 2026
93a507d
fix(tests): isolate database ownership fixtures
ScriptedAlchemy Jul 14, 2026
31fa599
fix(hermes): validate markdown tool envelopes
ScriptedAlchemy Jul 14, 2026
4c39107
fix(storage): stabilize daemon-owned database access
ScriptedAlchemy Jul 14, 2026
49e26a1
fix(daemon): reuse global database authority
ScriptedAlchemy Jul 14, 2026
b0e25b3
test(daemon): share initialize registry authority
ScriptedAlchemy Jul 14, 2026
aba92b9
fix(daemon): release legacy sync ownership after writes
ScriptedAlchemy Jul 14, 2026
2ab7a9f
fix(storage): harden cross-platform daemon authority
ScriptedAlchemy Jul 14, 2026
5d2c65d
fix(tests): preserve platform daemon lifecycle
ScriptedAlchemy Jul 14, 2026
0153869
test(storage): start branch daemon on Windows
ScriptedAlchemy Jul 14, 2026
e6fe9a3
test(mcp): canonicalize registry path assertions
ScriptedAlchemy Jul 14, 2026
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
4 changes: 2 additions & 2 deletions .config/nextest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,12 @@ filter = 'binary(=core_cli_suite) & test(/^tool_daemon_test::(daemon_socket_is_o
threads-required = "num-cpus"

[[profile.ci.overrides]]
filter = '(binary(=core_cli_suite) & test(/^cli_non_interactive_test::/)) | (binary(=mcp_suite) & test(/^(mcp_cli_serve_test|serve_degraded_mode_test|serve_template_path_test)::/))'
filter = '(binary(=core_cli_suite) & test(/^cli_non_interactive_test::/)) | (binary(=mcp_suite) & test(/^(mcp_cli_serve_test|serve_template_path_test)::/))'
test-group = 'cli-subprocess'
threads-required = "num-cpus"

[[profile.default.overrides]]
filter = '(binary(=core_cli_suite) & test(/^cli_non_interactive_test::/)) | (binary(=mcp_suite) & test(/^(mcp_cli_serve_test|serve_degraded_mode_test|serve_template_path_test)::/))'
filter = '(binary(=core_cli_suite) & test(/^cli_non_interactive_test::/)) | (binary(=mcp_suite) & test(/^(mcp_cli_serve_test|serve_template_path_test)::/))'
test-group = 'cli-subprocess'
threads-required = "num-cpus"

Expand Down
217 changes: 169 additions & 48 deletions dashboard/smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ const VIEWPORT_PROFILES = {
};

const DASHBOARD_URL_RE = /(http:\/\/127\.0\.0\.1:\d+\/)/;
const DAEMON_HARNESS_ACTIVE = "TRACEDECAY_DAEMON_HARNESS_ACTIVE";
const DASHBOARD_STARTUP_TIMEOUT_MS = 30_000;
const DASHBOARD_STOP_TIMEOUT_MS = 5_000;
const IS_UNIX = process.platform !== "win32";

function workspaceRoot() {
return fileURLToPath(new URL("..", import.meta.url));
Expand All @@ -40,6 +44,49 @@ function withTrailingSlash(url) {
return url.endsWith("/") ? url : `${url}/`;
}

function runnableFile(candidate) {
if (!candidate) return null;
const resolved = path.resolve(candidate);
try {
if (!fs.statSync(resolved).isFile()) return null;
if (IS_UNIX) fs.accessSync(resolved, fs.constants.X_OK);
return resolved;
} catch {
return null;
}
}

function resolveTracedecayBinary() {
const cargoBinary = runnableFile(process.env.CARGO_BIN_EXE_tracedecay);
if (cargoBinary) return cargoBinary;

const suffix = process.platform === "win32" ? ".exe" : "";
const workspaceBinary = path.join(workspaceRoot(), "target", "debug", `tracedecay${suffix}`);
const builtBinary = runnableFile(workspaceBinary);
if (builtBinary) return builtBinary;

throw new Error(`built TraceDecay binary not found at ${workspaceBinary}`);
}

function runUnderIsolatedDaemon() {
const harness = path.join(workspaceRoot(), "scripts", "with-isolated-tracedecay-daemon.sh");
const tracedecayBinary = resolveTracedecayBinary();
const result = spawnSync(
harness,
["--bin", tracedecayBinary, "--", process.execPath, ...process.argv.slice(1)],
{
cwd: workspaceRoot(),
env: process.env,
stdio: "inherit",
},
);
if (result.error) throw result.error;
if (result.signal) {
throw new Error(`isolated daemon harness terminated by ${result.signal}`);
}
return result.status ?? 1;
}

// The dashboard refuses to start without a TraceDecay index, and CI checkouts
// (unlike dev workspaces) have no `.tracedecay/`. Build a tiny throwaway
// project and index it so the smoke run is hermetic everywhere.
Expand All @@ -51,71 +98,137 @@ function createSmokeWorkspace() {
);
// stdin is closed so init's interactive `.gitignore` prompt reads EOF and
// proceeds with the default instead of blocking.
const result = spawnSync("cargo", ["run", "--", "init", dir], {
const result = spawnSync(resolveTracedecayBinary(), ["init", dir], {
cwd: workspaceRoot(),
env: process.env,
stdio: ["ignore", "inherit", "inherit"],
});
if (result.status !== 0) {
if (result.error || result.status !== 0) {
fs.rmSync(dir, { recursive: true, force: true });
if (result.error) throw result.error;
throw new Error(`tracedecay init failed for smoke workspace (code ${result.status})`);
}
return dir;
}

async function startDashboardServer(projectPath) {
return new Promise((resolve, reject) => {
const child = spawn(
"cargo",
["run", "--", "dashboard", "--port", "0", "--path", projectPath],
{
cwd: workspaceRoot(),
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
},
);
const child = spawn(
resolveTracedecayBinary(),
["dashboard", "--port", "0", "--path", projectPath],
{
cwd: workspaceRoot(),
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
detached: IS_UNIX,
},
);

let settled = false;
let stderrBuffer = "";
const complete = (handler, value) => {
if (settled) return;
settled = true;
handler(value);
};
let closed = false;
let stderrBuffer = "";
let stopPromise = null;
const exitPromise = new Promise((resolve) => {
child.once("error", (error) => resolve({ error }));
child.once("exit", (code, signal) => resolve({ code, signal }));
});
child.once("close", () => {
closed = true;
});
child.stderr.on("data", (chunk) => {
stderrBuffer += chunk.toString();
});

const stdoutLines = readline.createInterface({ input: child.stdout });
stdoutLines.on("line", (line) => {
process.stdout.write(`[dashboard] ${line}\n`);
const match = line.match(DASHBOARD_URL_RE);
if (match) {
complete(resolve, {
baseUrl: withTrailingSlash(match[1]),
child,
stop: async () => {
child.kill("SIGTERM");
await new Promise((done) => child.once("exit", done));
},
});
}
});
const stdoutLines = readline.createInterface({ input: child.stdout });
const stderrLines = readline.createInterface({ input: child.stderr });
stderrLines.on("line", (line) => process.stderr.write(`[dashboard:stderr] ${line}\n`));

const stderrLines = readline.createInterface({ input: child.stderr });
stderrLines.on("line", (line) => {
stderrBuffer = `${stderrBuffer}${line}\n`;
process.stderr.write(`[dashboard:stderr] ${line}\n`);
});
const processGroupAlive = () => {
if (child.pid === undefined) return false;
if (!IS_UNIX) return child.exitCode === null && child.signalCode === null;
try {
process.kill(-child.pid, 0);
return true;
} catch (error) {
if (error.code === "ESRCH") return false;
if (error.code === "EPERM") return true;
throw error;
}
};
const sendSignal = (signal) => {
if (child.pid === undefined) return;
try {
if (IS_UNIX) process.kill(-child.pid, signal);
else child.kill(signal);
} catch (error) {
if (error.code !== "ESRCH") throw error;
}
};
const waitForStop = async () => {
const deadline = Date.now() + DASHBOARD_STOP_TIMEOUT_MS;
while (Date.now() < deadline) {
if (closed && !processGroupAlive()) return true;
await new Promise((resolve) => setTimeout(resolve, 50));
}
return closed && !processGroupAlive();
};
const stop = () => {
if (stopPromise) return stopPromise;
stopPromise = (async () => {
try {
if (!closed || processGroupAlive()) sendSignal("SIGTERM");
if (!(await waitForStop())) {
sendSignal("SIGKILL");
if (!(await waitForStop())) {
throw new Error("dashboard server did not stop after SIGKILL");
}
}
} finally {
stdoutLines.close();
stderrLines.close();
}
})();
return stopPromise;
};

child.once("error", (err) => {
complete(reject, err);
});
child.once("exit", (code) => {
if (settled) return;
complete(
reject,
new Error(`dashboard server exited before startup (code ${code})\n${stderrBuffer}`),
try {
const baseUrl = await new Promise((resolve, reject) => {
let settled = false;
const complete = (handler, value) => {
if (settled) return;
settled = true;
clearTimeout(timer);
handler(value);
};
const timer = setTimeout(
() => complete(reject, new Error(
`dashboard server startup timed out after ${DASHBOARD_STARTUP_TIMEOUT_MS}ms`,
)),
DASHBOARD_STARTUP_TIMEOUT_MS,
);
stdoutLines.on("line", (line) => {
process.stdout.write(`[dashboard] ${line}\n`);
const match = line.match(DASHBOARD_URL_RE);
if (match) complete(resolve, withTrailingSlash(match[1]));
});
exitPromise.then(({ code, signal, error }) => {
const detail = error?.message ?? `code ${code}${signal ? `, signal ${signal}` : ""}`;
complete(reject, new Error(`dashboard server exited before startup (${detail})`));
});
});
});
return { baseUrl, child, stop };
} catch (error) {
let stopError = null;
try {
await stop();
} catch (caught) {
stopError = caught;
}
const diagnostics = stderrBuffer.trim();
const message = error instanceof Error ? error.message : String(error);
const stopMessage = stopError ? `\nshutdown error: ${stopError.message}` : "";
throw new Error(`${message}${diagnostics ? `\n${diagnostics}` : ""}${stopMessage}`, {
cause: error,
});
}
}

async function waitForAny(page, locators, timeoutMs) {
Expand Down Expand Up @@ -357,6 +470,13 @@ async function main() {
}
return profile;
});

if (!explicitUrl && process.env[DAEMON_HARNESS_ACTIVE] !== "1") {
console.log("Starting hermetic foreground TraceDecay daemon for smoke test...");
process.exitCode = runUnderIsolatedDaemon();
return;
}

let server = null;
let workspace = null;

Expand All @@ -365,6 +485,7 @@ async function main() {
server = { baseUrl: explicitUrl, stop: async () => {} };
console.log(`Using existing dashboard URL: ${explicitUrl}`);
} else {
console.log("Smoke daemon is ready.");
console.log("Creating hermetic smoke workspace (tracedecay init)...");
workspace = createSmokeWorkspace();
console.log(`Starting \`tracedecay dashboard --port 0 --path ${workspace}\` for smoke test...`);
Expand Down
Loading
Loading