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:
@@ -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 E2E;Context Runtime shadow Portal E2E(events-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` headless:OK(列 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 或从该分支继续开发。
|
||||
|
||||
|
||||
@@ -64,6 +64,18 @@ node scripts/check-codebase-memory-mcp-local.mjs
|
||||
node scripts/check-dsh-executor-local.mjs
|
||||
```
|
||||
|
||||
## Shadow Portal E2E(1 轮,省 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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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');
|
||||
Reference in New Issue
Block a user