feat(dev-tools): add shadow portal and dsh gateway local E2E scripts

Add events-only Context Runtime shadow portal verification to save LLM
tokens, wire Tool Gateway → dsh headless smoke, and document both flows.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-09-10 09:24:57 +08:00
parent c0d43379b8
commit f3348f45c0
5 changed files with 371 additions and 1 deletions
+26 -1
View File
@@ -962,7 +962,7 @@ Goose v1.49 本地 canary 收口、Context Runtime + Agent Harness 融合 Phase
审计日期:2026-09-10
分支 HEAD`47d0d2ac`
`origin/main` 对应提交:`47d0d2ac`(待 push
`origin/main` 对应提交:`938f6d6d`
### 原始用途
@@ -978,3 +978,28 @@ fusion-plan §6.6 本机开发工具:`codebase-memory-mcp` 本地安装验证
- 保留本地分支名用于审计追溯。
- **不要** merge、cherry-pick 或从该分支继续开发。
## `feature/dsh-executor-local-setup`
**状态:禁止再次引用。改动已提交并进入 `origin/main`,该分支保留仅用于只读追溯,不是待合并开发分支。**
审计日期:2026-09-10
分支 HEAD:(见 merge commit
`origin/main` 对应提交:(见 merge commit
### 原始用途
`@deepseek-ai/dsh@0.1.1-rc.2` 本机安装、check 脚本、Tool Gateway headless E2EContext Runtime shadow Portal E2Eevents-only 省 token)。
### 验证摘要
- `dsh --version`0.1.1-rc.2
- `node scripts/check-dsh-executor-local.mjs``DSH_EXECUTOR_CLI_OK` / dry-run OK
- `MEMIND_DSH_RUN_LIVE_SMOKE=1` headlessOK(列 3 文件)
- `MEMIND_TOOL_GATEWAY_DSH_ENABLED=1 node scripts/test-dsh-executor-gateway-e2e.mjs``DSH_GATEWAY_E2E_OK`
- Shadow Portal`force_deep_reasoning`run `2ca8be7b…`):`headroom_context_observed` + `context_budget_resolved``mode=shadow`
### 最终处置
- 保留本地分支名用于审计追溯。
- **不要** merge、cherry-pick 或从该分支继续开发。
+12
View File
@@ -64,6 +64,18 @@ node scripts/check-codebase-memory-mcp-local.mjs
node scripts/check-dsh-executor-local.mjs
```
## Shadow Portal E2E1 轮,省 token 默认只验事件)
Portal 需带 shadow 五件套(见上文)运行 `pnpm dev`
```bash
node scripts/run-context-runtime-shadow-portal-e2e.mjs
# 默认 CONTEXT_RUNTIME_SHADOW_EVENTS_ONLY=1:拿到 headroom/budget shadow 事件即停,不等 Goose 跑完
# 通过 → CONTEXT_RUNTIME_SHADOW_PORTAL_OK
```
`recall_fusion_resolved` 在 agent memory 关闭时可能缺失,脚本会 WARN 但不阻断。
## 用户自测清单(有 LLM 配额时)
1. 确认 **未设置** `GOOSE_V149_ALLOW_REAL_LLM=1` 除非你明确要跑 live smoke
+9
View File
@@ -62,6 +62,15 @@ node scripts/check-dsh-executor-local.mjs
- **预计超过 10k token 必须先经用户同意**
- 禁止循环重跑、禁止 unattended `check-goosed-v149-all` / phase3 聚合
## Tool Gateway E2E(一次 headless
```bash
MEMIND_TOOL_GATEWAY_DSH_ENABLED=1 node scripts/test-dsh-executor-gateway-e2e.mjs
# → DSH_GATEWAY_E2E_OK
```
路由 `repo_refactor``dsh`;会消耗 LLM token,须人工批准。
## headless 手动探针
```bash
@@ -0,0 +1,232 @@
#!/usr/bin/env node
/**
* Context Runtime shadow portal E2E: one agent turn, verify shadow events in DB.
* Default: poll shadow events only (do not wait for full Goose completion — saves LLM tokens).
*/
import { randomUUID } from 'node:crypto';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createDbPool } from '../db.mjs';
import { CONTEXT_RUNTIME_SHADOW_ENV } from '../context-runtime-profile.mjs';
import {
createReporter,
loginViaApi,
resolvePortalBase,
sleep,
waitForRunTerminal,
} from './scenario-test-lib.mjs';
import { waitForAgentRunWorkerIdle } from './goose-v149-worker-idle.mjs';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
for (const [key, value] of Object.entries(CONTEXT_RUNTIME_SHADOW_ENV)) {
process.env[key] = process.env[key] ?? value;
}
process.env.HEADROOM_OUTPUT_SHAPER = process.env.HEADROOM_OUTPUT_SHAPER ?? '0';
const baseUrl = resolvePortalBase(Number(process.env.H5_PORT ?? 8081));
const timeoutMs = Number(process.env.CONTEXT_RUNTIME_SHADOW_PORTAL_TIMEOUT_MS ?? 180_000);
const eventsOnly = process.env.CONTEXT_RUNTIME_SHADOW_EVENTS_ONLY !== '0';
const pollMs = Number(process.env.CONTEXT_RUNTIME_SHADOW_POLL_MS ?? 2000);
async function portalReachable() {
try {
const response = await fetch(`${baseUrl}/auth/status`);
return response.ok;
} catch {
return false;
}
}
async function fetchRunEvents(pool, runId) {
const [rows] = await pool.query(
`SELECT event_type, data_json, created_at
FROM h5_agent_run_events
WHERE run_id = ?
ORDER BY created_at ASC`,
[runId],
);
return rows.map((row) => ({
eventType: row.event_type,
data: typeof row.data_json === 'string' ? JSON.parse(row.data_json) : row.data_json,
createdAt: Number(row.created_at),
}));
}
function pickEvents(events, type) {
return events.filter((event) => event.eventType === type);
}
function evaluateShadowEvents(events) {
const headroomEvents = pickEvents(events, 'headroom_context_observed');
const budgetEvents = pickEvents(events, 'context_budget_resolved');
const fusionEvents = pickEvents(events, 'recall_fusion_resolved');
const issues = [];
const warnings = [];
if (!headroomEvents.length) issues.push('missing headroom_context_observed');
if (!budgetEvents.length) issues.push('missing context_budget_resolved');
if (!fusionEvents.length) {
warnings.push('missing recall_fusion_resolved (memory path may be off/skipped)');
}
for (const [label, rows] of [
['headroom', headroomEvents],
['budget', budgetEvents],
['fusion', fusionEvents],
]) {
const mode = rows[0]?.data?.mode;
if (mode && mode !== 'shadow') {
issues.push(`${label} mode=${mode} (expected shadow)`);
}
}
return { headroomEvents, budgetEvents, fusionEvents, issues, warnings };
}
async function pollShadowEvents(pool, runId, deadlineMs) {
while (Date.now() < deadlineMs) {
const events = await fetchRunEvents(pool, runId);
const evaluation = evaluateShadowEvents(events);
const coreReady = evaluation.headroomEvents.length && evaluation.budgetEvents.length;
if (coreReady) {
return { events, evaluation, done: true };
}
await sleep(pollMs);
}
const events = await fetchRunEvents(pool, runId);
return { events, evaluation: evaluateShadowEvents(events), done: false };
}
async function runPortalTurn(pool) {
const username = process.env.RELEASE_GATE_SCENARIO_USERNAME ?? 'john';
const password =
process.env.JOHN_PASSWORD
?? process.env.H5_ACCESS_PASSWORD
?? process.env.MEMIND_PASSWORD
?? '';
if (!password) throw new Error('set JOHN_PASSWORD for portal e2e');
const reporter = createReporter();
const auth = await loginViaApi(baseUrl, { username, password }, reporter);
await waitForAgentRunWorkerIdle(root, process.env, {
logPrefix: '[context-runtime-shadow-e2e]',
});
const startRes = await fetch(`${baseUrl}/api/agent/start`, {
method: 'POST',
headers: { Cookie: auth.cookie, 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
const started = await startRes.json().catch(() => ({}));
if (!startRes.ok || !started?.id) {
throw new Error(`agent/start failed: ${startRes.status}`);
}
const sessionId = started.id;
const warmResume = await fetch(`${baseUrl}/api/agent/resume`, {
method: 'POST',
headers: { Cookie: auth.cookie, 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: sessionId,
load_model_and_extensions: true,
}),
});
if (!warmResume.ok) {
const warmBody = await warmResume.text().catch(() => '');
throw new Error(`pre-run resume failed: ${warmResume.status} ${warmBody.slice(0, 200)}`);
}
const requestId = randomUUID();
const runRes = await fetch(`${baseUrl}/api/agent/runs`, {
method: 'POST',
headers: { Cookie: auth.cookie, 'Content-Type': 'application/json' },
body: JSON.stringify({
request_id: requestId,
session_id: sessionId,
force_deep_reasoning: true,
user_message: {
id: randomUUID(),
role: 'user',
content: [{ type: 'text', text: '帮我搜索一下今天上海的天气,简要回答即可' }],
metadata: {
userVisible: true,
agentVisible: true,
displayText: 'context runtime shadow agent ping',
},
},
}),
});
const runPayload = await runRes.json().catch(() => ({}));
if (!runRes.ok) {
throw new Error(`POST /api/agent/runs ${runRes.status}: ${JSON.stringify(runPayload).slice(0, 300)}`);
}
const runId = runPayload.run?.id ?? runPayload.id;
if (eventsOnly) {
const polled = await pollShadowEvents(pool, runId, Date.now() + Math.min(timeoutMs, 60_000));
return {
sessionId,
runId,
terminal: { status: polled.done ? 'shadow_events_ready' : 'shadow_events_timeout' },
events: polled.events,
evaluation: polled.evaluation,
eventsOnly: true,
};
}
const terminal = await waitForRunTerminal(baseUrl, auth.cookie, runId, timeoutMs);
const events = await fetchRunEvents(pool, runId);
return {
sessionId,
runId,
terminal,
events,
evaluation: evaluateShadowEvents(events),
eventsOnly: false,
};
}
async function main() {
if (!(await portalReachable())) {
throw new Error(`Portal not reachable at ${baseUrl} — start pnpm dev first`);
}
const pool = createDbPool();
const portal = await runPortalTurn(pool);
const { headroomEvents, budgetEvents, fusionEvents, issues, warnings } = portal.evaluation;
console.log('CONTEXT_RUNTIME_SHADOW_PORTAL_E2E:');
console.log(` portal=${baseUrl}`);
console.log(` run_id=${portal.runId}`);
console.log(` mode=${portal.eventsOnly ? 'events_only' : 'full_terminal'}`);
console.log(` terminal=${portal.terminal.status}`);
console.log(` headroom_events=${headroomEvents.length}`);
console.log(` budget_events=${budgetEvents.length}`);
console.log(` fusion_events=${fusionEvents.length}`);
if (headroomEvents[0]?.data) {
console.log(` headroom_sample=${JSON.stringify(headroomEvents[0].data)}`);
}
if (budgetEvents[0]?.data) {
console.log(` budget_sample=${JSON.stringify(budgetEvents[0].data)}`);
}
if (fusionEvents[0]?.data) {
console.log(` fusion_sample=${JSON.stringify(fusionEvents[0].data)}`);
}
for (const warning of warnings) {
console.warn(`CONTEXT_RUNTIME_SHADOW_PORTAL_WARN: ${warning}`);
}
if (issues.length) {
console.error(`CONTEXT_RUNTIME_SHADOW_PORTAL_FAIL: ${issues.join('; ')}`);
process.exit(1);
}
console.log('CONTEXT_RUNTIME_SHADOW_PORTAL_OK');
}
main().catch((err) => {
console.error(err instanceof Error ? err.message : err);
process.exit(1);
});
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env node
/**
* Minimal Tool Gateway → dsh executor E2E (one headless task, costs LLM tokens).
*
* Usage:
* MEMIND_TOOL_GATEWAY_DSH_ENABLED=1 \
* node scripts/test-dsh-executor-gateway-e2e.mjs
*/
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { buildDshExecutorLaunchPlan } from '../dsh-agent-launch.mjs';
import { createToolGateway } from '../tool-gateway.mjs';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const cwd = process.argv[2] ?? root;
const instruction = process.env.MEMIND_DSH_GATEWAY_TASK
?? 'List three files in the workspace root and stop. Do not modify any files.';
process.env.MEMIND_TOOL_GATEWAY_ENABLED = process.env.MEMIND_TOOL_GATEWAY_ENABLED ?? '1';
process.env.MEMIND_TOOL_GATEWAY_DRY_RUN = '0';
process.env.MEMIND_TOOL_GATEWAY_DSH_ENABLED = process.env.MEMIND_TOOL_GATEWAY_DSH_ENABLED ?? '1';
process.env.MEMIND_TOOL_GATEWAY_DSH_TASK_TYPES = process.env.MEMIND_TOOL_GATEWAY_DSH_TASK_TYPES ?? 'repo_refactor';
const gateway = createToolGateway({
env: process.env,
llmProviderService: {
async getExecutorLaunchPlan(executor, options) {
if (executor !== 'dsh') {
throw new Error(`unexpected executor ${executor}`);
}
return buildDshExecutorLaunchPlan({
cwd: options.cwd,
instruction: options.instruction,
env: process.env,
runtimeEnv: {
DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY ?? process.env.OPENAI_API_KEY ?? '',
},
});
},
},
});
const status = gateway.getStatus();
console.log('DSH_GATEWAY_E2E_PROBE:');
console.log(JSON.stringify({
enabled: status.enabled,
dryRun: status.dryRun,
executors: status.executors,
dshEnabled: status.dshEnabled,
dshTaskTypes: status.dshTaskTypes,
selected: gateway.selectExecutor({ taskType: 'repo_refactor' }),
}, null, 2));
if (!status.dshEnabled) {
console.error('DSH_GATEWAY_E2E_FAIL: MEMIND_TOOL_GATEWAY_DSH_ENABLED is off');
process.exit(1);
}
const result = await gateway.executeJob({
runId: 'dsh-gateway-e2e',
requestId: 'dsh-gateway-req',
userId: 'local-user',
cwd,
taskType: 'repo_refactor',
timeoutMs: Number(process.env.MEMIND_DSH_GATEWAY_TIMEOUT_MS ?? 180_000),
userMessage: {
content: [{ type: 'text', text: instruction }],
metadata: {
memindRun: {
taskType: 'repo_refactor',
toolMode: 'code',
},
},
},
});
const stdoutPreview = String(result.stdout ?? result.displayStdout ?? '').trim().slice(0, 500);
console.log('DSH_GATEWAY_E2E_RESULT:');
console.log(JSON.stringify({
ok: result.ok,
executor: result.executor,
exitCode: result.exitCode,
stdoutPreview,
}, null, 2));
if (!result.ok || result.executor !== 'dsh') {
console.error('DSH_GATEWAY_E2E_FAIL');
process.exit(1);
}
console.log('DSH_GATEWAY_E2E_OK');