Makelore 2.0 initial clean snapshot
This commit is contained in:
202
scripts/opencode-real-runtime-smoke.mjs
Normal file
202
scripts/opencode-real-runtime-smoke.mjs
Normal file
@@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawn } from 'node:child_process';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createServer } from 'node:net';
|
||||
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const enabled = process.env.NIANCODE_OPENCODE_REAL_SMOKE === '1'
|
||||
|| process.env.OPENCODE_REAL_SMOKE === '1';
|
||||
|
||||
function skip(reason) {
|
||||
console.log(`[opencode-real-smoke] skipped: ${reason}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
skip('set NIANCODE_OPENCODE_REAL_SMOKE=1 to launch a real opencode runtime');
|
||||
}
|
||||
|
||||
function resolveOpencodeBin() {
|
||||
const explicit = process.env.NIANCODE_OPENCODE_BIN || process.env.OPENCODE_BIN;
|
||||
if (explicit) return explicit;
|
||||
|
||||
const binName = process.platform === 'win32' ? 'opencode.cmd' : 'opencode';
|
||||
const localBin = join(repoRoot, 'node_modules', '.bin', binName);
|
||||
if (existsSync(localBin)) return localBin;
|
||||
return binName;
|
||||
}
|
||||
|
||||
async function resolveConfigContent() {
|
||||
if (process.env.OPENCODE_CONFIG_CONTENT) return process.env.OPENCODE_CONFIG_CONTENT;
|
||||
if (process.env.OPENCODE_REAL_SMOKE_CONFIG) return process.env.OPENCODE_REAL_SMOKE_CONFIG;
|
||||
if (process.env.OPENCODE_REAL_SMOKE_CONFIG_FILE) {
|
||||
return await readFile(resolve(process.env.OPENCODE_REAL_SMOKE_CONFIG_FILE), 'utf8');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function getFreePort() {
|
||||
return await new Promise((resolvePort, reject) => {
|
||||
const server = createServer();
|
||||
server.once('error', reject);
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
const address = server.address();
|
||||
const port = typeof address === 'object' && address ? address.port : null;
|
||||
server.close(() => {
|
||||
if (typeof port === 'number') {
|
||||
resolvePort(port);
|
||||
return;
|
||||
}
|
||||
reject(new Error('Unable to reserve a local smoke-test port'));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function waitForListening(proc, timeoutMs) {
|
||||
return awaitableProcessOutput(proc, timeoutMs, (text) => {
|
||||
const match = text.match(/opencode server listening\s+on\s+(https?:\/\/[^\s]+)/);
|
||||
return match?.[1] ?? null;
|
||||
});
|
||||
}
|
||||
|
||||
function awaitableProcessOutput(proc, timeoutMs, resolveFromText) {
|
||||
return new Promise((resolveUrl, reject) => {
|
||||
let settled = false;
|
||||
let stderr = '';
|
||||
const finish = (callback) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
callback();
|
||||
};
|
||||
const timer = setTimeout(() => {
|
||||
finish(() => reject(new Error(`Timed out waiting for opencode server after ${timeoutMs}ms${stderr ? `\n${stderr}` : ''}`)));
|
||||
}, timeoutMs);
|
||||
|
||||
proc.stdout?.on('data', (chunk) => {
|
||||
const text = chunk.toString();
|
||||
process.stdout.write(text);
|
||||
const result = resolveFromText(text);
|
||||
if (result) finish(() => resolveUrl(result));
|
||||
});
|
||||
proc.stderr?.on('data', (chunk) => {
|
||||
const text = chunk.toString();
|
||||
stderr += text;
|
||||
process.stderr.write(text);
|
||||
});
|
||||
proc.on('error', (error) => finish(() => reject(error)));
|
||||
proc.on('exit', (code) => {
|
||||
finish(() => reject(new Error(`opencode exited before it became ready: ${code ?? 'unknown'}${stderr ? `\n${stderr}` : ''}`)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sessionIdOf(session) {
|
||||
if (!session || typeof session !== 'object') return null;
|
||||
const value = session.id ?? session.sessionID ?? session.sessionId;
|
||||
return typeof value === 'string' && value.trim() ? value : null;
|
||||
}
|
||||
|
||||
async function requestJson(url, init = {}) {
|
||||
const response = await fetch(url, {
|
||||
...init,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(init.headers ?? {}),
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = await response.text().catch(() => '');
|
||||
throw new Error(`opencode request failed ${response.status} ${response.statusText}${body ? `\n${body}` : ''}`);
|
||||
}
|
||||
if (response.status === 204) return null;
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async function createProjectDir() {
|
||||
if (process.env.OPENCODE_REAL_SMOKE_PROJECT) {
|
||||
return {
|
||||
path: resolve(process.env.OPENCODE_REAL_SMOKE_PROJECT),
|
||||
cleanup: async () => {},
|
||||
};
|
||||
}
|
||||
|
||||
const path = await mkdtemp(join(tmpdir(), 'niancode-opencode-smoke-'));
|
||||
await writeFile(join(path, 'README.md'), '# Makelore opencode real-runtime smoke\n');
|
||||
return {
|
||||
path,
|
||||
cleanup: async () => {
|
||||
await rm(path, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const configContent = await resolveConfigContent();
|
||||
if (!configContent && process.env.OPENCODE_REAL_SMOKE_ALLOW_LOCAL_CONFIG !== '1') {
|
||||
skip('provide OPENCODE_CONFIG_CONTENT, OPENCODE_REAL_SMOKE_CONFIG, or OPENCODE_REAL_SMOKE_CONFIG_FILE');
|
||||
}
|
||||
|
||||
JSON.parse(configContent ?? '{}');
|
||||
|
||||
const binPath = resolveOpencodeBin();
|
||||
const port = Number(process.env.OPENCODE_REAL_SMOKE_PORT) || await getFreePort();
|
||||
const startupTimeoutMs = Number(process.env.OPENCODE_REAL_SMOKE_STARTUP_TIMEOUT_MS) || 20_000;
|
||||
const apiTimeoutMs = Number(process.env.OPENCODE_REAL_SMOKE_API_TIMEOUT_MS) || 60_000;
|
||||
const project = await createProjectDir();
|
||||
const proc = spawn(binPath, ['serve', '--hostname=127.0.0.1', `--port=${port}`], {
|
||||
env: {
|
||||
...process.env,
|
||||
...(configContent ? { OPENCODE_CONFIG_CONTENT: configContent } : {}),
|
||||
},
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
windowsHide: true,
|
||||
shell: process.platform === 'win32' && /\.cmd$/i.test(binPath),
|
||||
});
|
||||
|
||||
try {
|
||||
const baseUrl = await waitForListening(proc, startupTimeoutMs);
|
||||
const directoryQuery = `directory=${encodeURIComponent(project.path)}`;
|
||||
const scopedHeaders = { 'x-opencode-directory': encodeURIComponent(project.path) };
|
||||
|
||||
const sessions = await requestJson(`${baseUrl}/session?${directoryQuery}`, {
|
||||
signal: AbortSignal.timeout(apiTimeoutMs),
|
||||
headers: scopedHeaders,
|
||||
});
|
||||
if (!Array.isArray(sessions)) {
|
||||
throw new Error('Expected /session to return an array');
|
||||
}
|
||||
|
||||
const session = await requestJson(`${baseUrl}/session?${directoryQuery}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({}),
|
||||
signal: AbortSignal.timeout(apiTimeoutMs),
|
||||
headers: scopedHeaders,
|
||||
});
|
||||
const sessionId = sessionIdOf(session);
|
||||
if (!sessionId) {
|
||||
throw new Error('Created opencode session did not include an id');
|
||||
}
|
||||
|
||||
if (process.env.OPENCODE_REAL_SMOKE_PROMPT) {
|
||||
await requestJson(`${baseUrl}/session/${encodeURIComponent(sessionId)}/message?${directoryQuery}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
parts: [
|
||||
{ type: 'text', text: process.env.OPENCODE_REAL_SMOKE_PROMPT },
|
||||
],
|
||||
}),
|
||||
signal: AbortSignal.timeout(apiTimeoutMs),
|
||||
headers: scopedHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[opencode-real-smoke] ok: started ${baseUrl}, listed ${sessions.length} session(s), created ${sessionId}`);
|
||||
} finally {
|
||||
proc.kill();
|
||||
await project.cleanup();
|
||||
}
|
||||
Reference in New Issue
Block a user