mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-20 16:47:59 +02:00
Merge branch 'main' into fix/prepush-venv-pytest
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
const assert = require("assert")
|
||||
const fs = require("fs")
|
||||
const os = require("os")
|
||||
const path = require("path")
|
||||
const { spawnSync } = require("child_process")
|
||||
const { getNpmPackEntry } = require("../lib/npm-pack-output")
|
||||
@@ -46,6 +47,72 @@ function main() {
|
||||
assert.strictEqual(result.status, 0, result.stderr)
|
||||
assert.ok(fs.existsSync(distEntry), ".opencode/dist/index.js should exist after build")
|
||||
}],
|
||||
["package.json declares a resolvable OpenCode plugin entry", () => {
|
||||
assert.strictEqual(packageJson.main, ".opencode/dist/index.js")
|
||||
assert.ok(packageJson.exports, "package.json must declare an exports map")
|
||||
assert.deepStrictEqual(packageJson.exports["."], {
|
||||
types: "./.opencode/dist/index.d.ts",
|
||||
import: "./.opencode/dist/index.js",
|
||||
default: "./.opencode/dist/index.js",
|
||||
})
|
||||
}],
|
||||
["installed package resolves and imports its root module by name", () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ecc-opencode-entry-"))
|
||||
try {
|
||||
fs.mkdirSync(path.join(tempDir, "node_modules"), { recursive: true })
|
||||
fs.symlinkSync(
|
||||
repoRoot,
|
||||
path.join(tempDir, "node_modules", "ecc-universal"),
|
||||
process.platform === "win32" ? "junction" : "dir"
|
||||
)
|
||||
const probe = `
|
||||
const resolved = import.meta.resolve("ecc-universal")
|
||||
if (!resolved.endsWith("/.opencode/dist/index.js")) {
|
||||
throw new Error("unexpected entry resolution: " + resolved)
|
||||
}
|
||||
const mod = await import("ecc-universal")
|
||||
if (Object.keys(mod).join(",") !== "default" || typeof mod.default !== "function") {
|
||||
throw new Error("root module must export exactly the plugin function")
|
||||
}
|
||||
`
|
||||
const probePath = path.join(tempDir, "probe.mjs")
|
||||
fs.writeFileSync(probePath, probe)
|
||||
const result = spawnSync(process.execPath, [probePath], {
|
||||
cwd: tempDir,
|
||||
encoding: "utf8",
|
||||
})
|
||||
assert.strictEqual(result.status, 0, result.stderr)
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true })
|
||||
}
|
||||
}],
|
||||
["OpenCode TypeScript sources resolve their relative imports in place", () => {
|
||||
const opencodeDir = path.join(repoRoot, ".opencode")
|
||||
const sourceFiles = []
|
||||
const walk = (dir) => {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const entryPath = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name !== "node_modules" && entry.name !== "dist") walk(entryPath)
|
||||
} else if (entry.name.endsWith(".ts")) {
|
||||
sourceFiles.push(entryPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(opencodeDir)
|
||||
assert.ok(sourceFiles.length > 0, "expected OpenCode TypeScript sources")
|
||||
const unresolved = []
|
||||
for (const sourceFile of sourceFiles) {
|
||||
const source = fs.readFileSync(sourceFile, "utf8")
|
||||
for (const match of source.matchAll(/(?:from|import)\s*\(?\s*"(\.[^"]+)"/g)) {
|
||||
const target = path.resolve(path.dirname(sourceFile), match[1])
|
||||
if (!fs.existsSync(target)) {
|
||||
unresolved.push(`${path.relative(repoRoot, sourceFile)} -> ${match[1]}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.deepStrictEqual(unresolved, [])
|
||||
}],
|
||||
["built OpenCode entry exports only the plugin function", () => {
|
||||
const check = `
|
||||
const assert = require("assert")
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
const assert = require('assert');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
const {
|
||||
collectInteractiveOptions,
|
||||
@@ -60,25 +60,56 @@ function quoteShellArgument(value) {
|
||||
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function runGuidedPtyFixture(answers) {
|
||||
if (process.platform === 'win32') return null;
|
||||
function stripPtyControlBytes(value) {
|
||||
return value
|
||||
// eslint-disable-next-line no-control-regex
|
||||
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '')
|
||||
.replace(/\r/g, '');
|
||||
}
|
||||
|
||||
function runGuidedPtyFixture(exchanges) {
|
||||
if (process.platform === 'win32') return Promise.resolve(null);
|
||||
const command = [process.execPath, guidedPtyFixture];
|
||||
const scriptArgs = process.platform === 'darwin'
|
||||
? ['-q', '-e', '/dev/null', ...command]
|
||||
: ['-q', '-e', '-c', command.map(quoteShellArgument).join(' '), '/dev/null'];
|
||||
const pseudoTerminalCommand = ['script', ...scriptArgs]
|
||||
.map(quoteShellArgument)
|
||||
.join(' ');
|
||||
const answerCommands = answers
|
||||
.map(answer => `sleep 0.35; printf '%s\\n' ${quoteShellArgument(answer)}`)
|
||||
.join('; ');
|
||||
return spawnSync('sh', ['-c', `(${answerCommands}; sleep 0.1) | ${pseudoTerminalCommand}`], {
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8',
|
||||
timeout: 15000,
|
||||
return new Promise((resolve, reject) => {
|
||||
// Answers go through cat so script reads a plain pipe: spawned stdio is
|
||||
// a socketpair, and the macOS script(1) refuses a socket stdin.
|
||||
const feeder = `cat | ${['script', ...scriptArgs].map(quoteShellArgument).join(' ')}`;
|
||||
const child = spawn('sh', ['-c', feeder], { cwd: repoRoot });
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let sent = 0;
|
||||
let settled = false;
|
||||
const finish = callback => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
callback();
|
||||
};
|
||||
const timer = setTimeout(() => {
|
||||
child.kill('SIGKILL');
|
||||
finish(() => reject(new Error('guided PTY fixture timed out')));
|
||||
}, 15000);
|
||||
const feed = () => {
|
||||
// Answer only once the matching prompt is on screen. Fixed sleeps
|
||||
// typed answers ahead of readline; under CI load the first answer
|
||||
// could land before the interface listened, shifting every later
|
||||
// answer onto the wrong question (ubuntu Node 18 npm job).
|
||||
const visible = stripPtyControlBytes(stdout + stderr);
|
||||
while (sent < exchanges.length && visible.includes(exchanges[sent].expect)) {
|
||||
child.stdin.write(`${exchanges[sent].send}\n`);
|
||||
sent += 1;
|
||||
}
|
||||
if (sent === exchanges.length) child.stdin.end();
|
||||
};
|
||||
child.stdout.on('data', data => { stdout += data; feed(); });
|
||||
child.stderr.on('data', data => { stderr += data; feed(); });
|
||||
child.on('error', error => finish(() => reject(error)));
|
||||
child.on('close', (status, signal) => finish(() => resolve({ status, signal, stdout, stderr })));
|
||||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
console.log('\n=== Guided multi-harness CLI tests ===\n');
|
||||
|
||||
@@ -175,14 +206,17 @@ function runGuidedPtyFixture(answers) {
|
||||
);
|
||||
});
|
||||
|
||||
await test('real PTY shows every all-harness question and applies after visible yes', () => {
|
||||
const result = runGuidedPtyFixture(['all', '1', '3', '2', 'y']);
|
||||
await test('real PTY shows every all-harness question and applies after visible yes', async () => {
|
||||
const result = await runGuidedPtyFixture([
|
||||
{ expect: 'Choose one or more (for example 1,3 or all):', send: 'all' },
|
||||
{ expect: 'Choose [Recommended: user] (one option only):', send: '1' },
|
||||
{ expect: 'Choose [Recommended: standard] (one option only):', send: '3' },
|
||||
{ expect: 'Choose [Recommended: core] (one option only):', send: '2' },
|
||||
{ expect: 'Apply ECC to these harnesses? [y/N]:', send: 'y' },
|
||||
]);
|
||||
if (result === null) return;
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
const visible = `${result.stdout}${result.stderr}`
|
||||
// eslint-disable-next-line no-control-regex
|
||||
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '')
|
||||
.replace(/\r/g, '');
|
||||
const visible = stripPtyControlBytes(`${result.stdout}${result.stderr}`);
|
||||
const orderedPrompts = [
|
||||
'Choose one or more (for example 1,3 or all):',
|
||||
'Choose [Recommended: user] (one option only):',
|
||||
|
||||
@@ -777,6 +777,8 @@ async function main() {
|
||||
},
|
||||
});
|
||||
assert.strictEqual(initialized.id, 0);
|
||||
assert.match(initialized.result.instructions, /host-bound harness identity/);
|
||||
assert.match(initialized.result.instructions, /does not provide OAuth/);
|
||||
await service.handle({
|
||||
jsonrpc: '2.0',
|
||||
method: 'notifications/initialized',
|
||||
|
||||
Reference in New Issue
Block a user