@galda/cli 0.10.82 → 0.10.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/engine/server.mjs +40 -8
- package/package.json +1 -1
package/engine/server.mjs
CHANGED
|
@@ -2982,12 +2982,22 @@ const testRunQueue = createSerialQueue();
|
|
|
2982
2982
|
// Manager-run tests (never trust the worker's "all green" claim): run the
|
|
2983
2983
|
// project's own `npm test` in the goal's worktree and parse node --test's
|
|
2984
2984
|
// "ℹ pass N / ℹ fail N" (or TAP "# pass/# fail"). Returns {ran,passed,failed,ok}.
|
|
2985
|
-
function
|
|
2985
|
+
function testNodePath(dir, dependencySourceDir = null) {
|
|
2986
|
+
// node_modules is intentionally absent from git worktrees. Reuse the base
|
|
2987
|
+
// checkout's installed dependencies before declaring every server/UI test a
|
|
2988
|
+
// product regression; verification must never silently depend on `npm install`.
|
|
2989
|
+
const sep = process.platform === 'win32' ? ';' : ':';
|
|
2990
|
+
const local = [join(dir, 'node_modules'), dependencySourceDir && join(dependencySourceDir, 'node_modules'), join(ROOT, 'node_modules')]
|
|
2991
|
+
.filter(Boolean).filter(existsSync);
|
|
2992
|
+
return [...new Set([...local, ...(process.env.NODE_PATH ? process.env.NODE_PATH.split(sep) : [])])].join(sep);
|
|
2993
|
+
}
|
|
2994
|
+
function runTestsOnce(dir, dependencySourceDir = null) {
|
|
2986
2995
|
// The timeout is env-overridable so tests can force the killed-run path fast;
|
|
2987
2996
|
// production keeps the 4-minute default.
|
|
2988
2997
|
const runTimeout = Number(process.env.MANAGER_TEST_TIMEOUT_MS) || 240000;
|
|
2989
2998
|
return testRunQueue.run(() => new Promise((res) => {
|
|
2990
|
-
|
|
2999
|
+
const nodePath = testNodePath(dir, dependencySourceDir);
|
|
3000
|
+
execFile('npm', ['test'], { cwd: dir, timeout: runTimeout, env: { ...process.env, ...(nodePath ? { NODE_PATH: nodePath } : {}) }, maxBuffer: 8 * 1024 * 1024 }, (e, out = '', err = '') => {
|
|
2991
3001
|
const text = `${out}\n${err}`;
|
|
2992
3002
|
const n = (re) => { const m = text.match(re); return m ? Number(m[1]) : null; };
|
|
2993
3003
|
const passed = n(/ℹ pass (\d+)/) ?? n(/# pass (\d+)/) ?? n(/(\d+) passing/) ?? 0;
|
|
@@ -3019,17 +3029,17 @@ function runTestsOnce(dir) {
|
|
|
3019
3029
|
});
|
|
3020
3030
|
}));
|
|
3021
3031
|
}
|
|
3022
|
-
async function runProjectTests(dir) {
|
|
3032
|
+
async function runProjectTests(dir, dependencySourceDir = null) {
|
|
3023
3033
|
let hasTest = false;
|
|
3024
3034
|
try { hasTest = !!JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).scripts?.test; } catch { /* no package.json */ }
|
|
3025
3035
|
if (!hasTest) return { ran: false };
|
|
3026
|
-
let r = await runTestsOnce(dir);
|
|
3036
|
+
let r = await runTestsOnce(dir, dependencySourceDir);
|
|
3027
3037
|
// Flake resistance: the integration suite spawns real servers; under load a
|
|
3028
3038
|
// startup can time out. Re-run once and take the fewer failures — a real
|
|
3029
3039
|
// failure fails both runs, a flake clears on the retry — so a good goal is
|
|
3030
3040
|
// never blocked on a flaky test.
|
|
3031
3041
|
if (r.failed > 0) {
|
|
3032
|
-
const r2 = await runTestsOnce(dir);
|
|
3042
|
+
const r2 = await runTestsOnce(dir, dependencySourceDir);
|
|
3033
3043
|
const kept = r2.failed <= r.failed ? r2 : r; // the run that saw fewer failures
|
|
3034
3044
|
r = {
|
|
3035
3045
|
passed: Math.max(r.passed, r2.passed),
|
|
@@ -3144,7 +3154,7 @@ async function runRetestJob(goal, token) {
|
|
|
3144
3154
|
} else {
|
|
3145
3155
|
for (let i = 0; i < RETEST_RUNS; i++) {
|
|
3146
3156
|
if (!live()) return; // cancelled (UNDO) / superseded / deleted → discard
|
|
3147
|
-
const r = await runTestsOnce(wdir);
|
|
3157
|
+
const r = await runTestsOnce(wdir, project.dir);
|
|
3148
3158
|
if (!live()) return; // cancelled while this run was in flight
|
|
3149
3159
|
results.push({ ran: true, passed: r.passed, failed: r.failed });
|
|
3150
3160
|
goal.retest = { state: 'running', runs: results.length, total: RETEST_RUNS, startedAt: goal.retest?.startedAt };
|
|
@@ -3509,7 +3519,7 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
|
|
|
3509
3519
|
const testGateOn = resolveTestGate({ reviewDefinition, env: process.env, force: forceTests });
|
|
3510
3520
|
if (testGateOn && hasTestRelevantChanges(changedFiles)) {
|
|
3511
3521
|
goalAct(`Running project tests in the goal worktree (${changedFiles.length} changed file${changedFiles.length === 1 ? '' : 's'}).`);
|
|
3512
|
-
goal.testResult = await runProjectTests(wdir);
|
|
3522
|
+
goal.testResult = await runProjectTests(wdir, project.dir);
|
|
3513
3523
|
goalAct(`Project tests finished: ${goal.testResult.failed ?? 0} failed, ${goal.testResult.passed ?? 0} passed.`);
|
|
3514
3524
|
} else if (!testGateOn) {
|
|
3515
3525
|
goal.testResult = { ran: false, skipped: true, skippedReason: 'test gate off (opt-in: requireTests / MANAGER_VERIFY_GATE=on)' };
|
|
@@ -4185,6 +4195,16 @@ const server = createServer(async (req, res) => {
|
|
|
4185
4195
|
for (const t of removedTasks) logAppend(JSON.stringify({ kind: 'task', id: t.id, deleted: true }));
|
|
4186
4196
|
projects.splice(idx, 1);
|
|
4187
4197
|
queues.delete(project.id);
|
|
4198
|
+
// A deleted project's goals must not outlive it: project ids are derived
|
|
4199
|
+
// from the name (uniqueProjectId), so a later "+ New project" click can
|
|
4200
|
+
// regenerate the exact same id and — without this purge — inherit the
|
|
4201
|
+
// old project's leftover goals as if they belonged to the brand-new one
|
|
4202
|
+
// (Masa report 2026-07-27: new project shows Review items on creation).
|
|
4203
|
+
for (const g of goals.filter((g) => g.projectId === project.id)) {
|
|
4204
|
+
goals = goals.filter((x) => x.id !== g.id);
|
|
4205
|
+
logAppend(JSON.stringify({ kind: 'goal', id: g.id, deleted: true }));
|
|
4206
|
+
sendGoalEvent({ ev: 'goal-deleted', id: g.id }, g);
|
|
4207
|
+
}
|
|
4188
4208
|
saveProjects();
|
|
4189
4209
|
send({ ev: 'projects', projects, deletedProjectId: project.id });
|
|
4190
4210
|
return json(res, 200, { ok: true, projects, deletedProjectId: project.id });
|
|
@@ -5418,7 +5438,19 @@ async function answerGoalQuestion(goal, question) {
|
|
|
5418
5438
|
if (upMatch) {
|
|
5419
5439
|
const file = join(logDir, 'uploads', upMatch[1]);
|
|
5420
5440
|
if (!existsSync(file)) { res.writeHead(404); return res.end('not found'); }
|
|
5421
|
-
|
|
5441
|
+
// Worker-declared Open it artifacts and image attachments share `/upload`.
|
|
5442
|
+
// Never guess that an unknown artifact is a PNG: it turns valid HTML, PDF,
|
|
5443
|
+
// or text into a broken-image page. Known preview formats get their real
|
|
5444
|
+
// browser type; an unknown file downloads safely instead of being misread.
|
|
5445
|
+
const ext = (file.match(/\.([a-z0-9]+)$/i)?.[1] || '').toLowerCase();
|
|
5446
|
+
const types = {
|
|
5447
|
+
html: 'text/html; charset=utf-8', htm: 'text/html; charset=utf-8',
|
|
5448
|
+
css: 'text/css; charset=utf-8', js: 'text/javascript; charset=utf-8', mjs: 'text/javascript; charset=utf-8',
|
|
5449
|
+
json: 'application/json; charset=utf-8', txt: 'text/plain; charset=utf-8', md: 'text/markdown; charset=utf-8', csv: 'text/csv; charset=utf-8',
|
|
5450
|
+
svg: 'image/svg+xml', png: 'image/png', gif: 'image/gif', jpg: 'image/jpeg', jpeg: 'image/jpeg', webp: 'image/webp', ico: 'image/x-icon',
|
|
5451
|
+
pdf: 'application/pdf', mp4: 'video/mp4', webm: 'video/webm', wasm: 'application/wasm',
|
|
5452
|
+
};
|
|
5453
|
+
res.writeHead(200, { 'content-type': types[ext] || 'application/octet-stream' });
|
|
5422
5454
|
return res.end(readFileSync(file));
|
|
5423
5455
|
}
|
|
5424
5456
|
|
package/package.json
CHANGED