fix: align DeepSeek canary tool rounds with gate
Memind CI / Test, build, and release guards (pull_request) Successful in 1m32s
Memind CI / Test, build, and release guards (pull_request) Successful in 1m32s
This commit is contained in:
@@ -6,6 +6,7 @@ import path from 'node:path';
|
||||
export const REQUIRED_PORTAL_RUNTIME_PATHS = Object.freeze([
|
||||
'server.mjs',
|
||||
'memind-canary-proxy.mjs',
|
||||
'deepseek-no-think-proxy.mjs',
|
||||
'wechat-mp.bundle.mjs',
|
||||
'mindspace-sandbox-mcp.mjs',
|
||||
'tkmind-search-mcp.mjs',
|
||||
@@ -16,6 +17,7 @@ export const REQUIRED_PORTAL_RUNTIME_PATHS = Object.freeze([
|
||||
'scripts/run-memind-portal-prod.sh',
|
||||
'scripts/run-memind-portal-candidate.sh',
|
||||
'scripts/run-memind-canary-proxy-prod.sh',
|
||||
'scripts/run-deepseek-compat-proxy-candidate.sh',
|
||||
'scripts/goosed-canary.compose.yml',
|
||||
'scripts/check-mindspace-public-links.mjs',
|
||||
'scripts/load-env.mjs',
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
import { startDeepseekNoThinkProxy } from '../deepseek-no-think-proxy.mjs';
|
||||
|
||||
const ROOT = path.resolve(new URL('..', import.meta.url).pathname);
|
||||
|
||||
test('sanitized production tool-round replay reaches DeepSeek with thinking disabled', async (t) => {
|
||||
const forwarded = [];
|
||||
const proxy = await startDeepseekNoThinkProxy({
|
||||
host: '127.0.0.1',
|
||||
port: 0,
|
||||
logger: { info() {}, warn() {}, error() {} },
|
||||
fetchImpl: async (_url, init) => {
|
||||
forwarded.push(JSON.parse(String(init.body)));
|
||||
return new Response(JSON.stringify({
|
||||
choices: [{ message: { role: 'assistant', content: 'synthetic result' } }],
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
},
|
||||
});
|
||||
t.after(() => new Promise((resolve) => proxy.close(resolve)));
|
||||
const address = proxy.address();
|
||||
|
||||
const response = await fetch(
|
||||
`http://127.0.0.1:${address.port}/v1/chat/completions`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
authorization: 'Bearer synthetic-key',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'deepseek-v4-pro',
|
||||
thinking: { type: 'enabled' },
|
||||
messages: [
|
||||
{ role: 'user', content: 'Use the synthetic filesystem tool.' },
|
||||
{ role: 'assistant', tool_calls: [{ id: 'call-1', type: 'function' }] },
|
||||
{ role: 'tool', tool_call_id: 'call-1', content: 'synthetic file list' },
|
||||
],
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(forwarded.length, 1);
|
||||
assert.deepEqual(forwarded[0].thinking, { type: 'disabled' });
|
||||
assert.equal(forwarded[0].messages[2].role, 'tool');
|
||||
});
|
||||
|
||||
test('Gate, artifact, candidate routing and rollback share one compatibility contract', async () => {
|
||||
const [
|
||||
builder,
|
||||
localStack,
|
||||
candidateRunner,
|
||||
compatRunner,
|
||||
canaryRelease,
|
||||
canaryRollback,
|
||||
] = await Promise.all([
|
||||
fs.readFile(path.join(ROOT, 'scripts', 'build-portal-runtime.mjs'), 'utf8'),
|
||||
fs.readFile(path.join(ROOT, 'release-gate', 'local-stack.mjs'), 'utf8'),
|
||||
fs.readFile(path.join(ROOT, 'scripts', 'run-memind-portal-candidate.sh'), 'utf8'),
|
||||
fs.readFile(
|
||||
path.join(ROOT, 'scripts', 'run-deepseek-compat-proxy-candidate.sh'),
|
||||
'utf8',
|
||||
),
|
||||
fs.readFile(path.join(ROOT, 'scripts', 'release-portal-canary-prod.sh'), 'utf8'),
|
||||
fs.readFile(path.join(ROOT, 'scripts', 'rollback-portal-canary-prod.sh'), 'utf8'),
|
||||
]);
|
||||
|
||||
assert.match(builder, /--outfile=\.runtime\/portal\/deepseek-no-think-proxy\.mjs/);
|
||||
assert.match(
|
||||
localStack,
|
||||
/path\.join\(resolvedPortalRoot, 'deepseek-no-think-proxy\.mjs'\)/,
|
||||
);
|
||||
assert.match(candidateRunner, /export MEMIND_DEEPSEEK_DISABLE_THINKING=1/);
|
||||
assert.match(candidateRunner, /export MEMIND_GOOSED_HOST_GATEWAY=host\.docker\.internal/);
|
||||
assert.match(compatRunner, /source "\$\{STABLE_ROOT\}\/\.env"/);
|
||||
assert.match(compatRunner, /export MEMIND_DEEPSEEK_PROXY_ENTRYPOINT=1/);
|
||||
assert.match(canaryRelease, /run-deepseek-compat-proxy-candidate\.sh/);
|
||||
assert.match(canaryRelease, /MEMIND_CANARY_CANDIDATE_HEALTH_URLS/);
|
||||
assert.match(canaryRelease, /host\.docker\.internal:\$\{DEEPSEEK_COMPAT_PORT\}\/health/);
|
||||
assert.match(canaryRelease, /bootout.*DEEPSEEK_COMPAT_LABEL/);
|
||||
assert.match(canaryRollback, /bootout.*DEEPSEEK_COMPAT_LABEL/);
|
||||
});
|
||||
@@ -380,7 +380,7 @@ export async function createLocalGateStack({
|
||||
deepseekProxyLogFd = fs.openSync(deepseekProxyLogPath, 'a');
|
||||
deepseekProxyChild = spawn(
|
||||
process.execPath,
|
||||
[path.join(root, 'deepseek-no-think-proxy.mjs')],
|
||||
[path.join(resolvedPortalRoot, 'deepseek-no-think-proxy.mjs')],
|
||||
{
|
||||
cwd: root,
|
||||
env: {
|
||||
|
||||
@@ -63,6 +63,16 @@ test('runtime builder materializes and declares required Linux ARM64 native pack
|
||||
assert.match(source, /缺少原生 \.node 文件/);
|
||||
});
|
||||
|
||||
test('runtime builder bundles the DeepSeek compatibility dependency into the artifact', async () => {
|
||||
const source = await fs.readFile(
|
||||
path.join(ROOT, 'scripts', 'build-portal-runtime.mjs'),
|
||||
'utf8',
|
||||
);
|
||||
assert.match(source, /bundleDeepseekCompatProxy/);
|
||||
assert.match(source, /--outfile=\.runtime\/portal\/deepseek-no-think-proxy\.mjs/);
|
||||
assert.match(source, /await bundleDeepseekCompatProxy\(\)/);
|
||||
});
|
||||
|
||||
test('packaged runtime gate isolates persistent roots and rejects artifact mutation', async () => {
|
||||
const localStackSource = await fs.readFile(
|
||||
path.join(ROOT, 'release-gate', 'local-stack.mjs'),
|
||||
@@ -70,7 +80,10 @@ test('packaged runtime gate isolates persistent roots and rejects artifact mutat
|
||||
);
|
||||
assert.match(localStackSource, /MEMIND_PORTAL_H5_ROOT: sandboxRoot/);
|
||||
assert.match(localStackSource, /MEMIND_DEEPSEEK_DISABLE_THINKING: '1'/);
|
||||
assert.match(localStackSource, /deepseek-no-think-proxy\.mjs/);
|
||||
assert.match(
|
||||
localStackSource,
|
||||
/path\.join\(resolvedPortalRoot, 'deepseek-no-think-proxy\.mjs'\)/,
|
||||
);
|
||||
assert.match(localStackSource, /MEMIND_ORCHESTRATOR_MODE: 'shadow'/);
|
||||
assert.match(localStackSource, /MEMIND_ORCHESTRATOR_PAGE_DATA_VALIDATION_GATE_ENABLED: '1'/);
|
||||
assert.match(localStackSource, /H5_USERS_ROOT: usersRoot/);
|
||||
@@ -119,6 +132,9 @@ test('production canary keeps stable 8081 live and switches only after verified
|
||||
const edgeBackup = source.indexOf('Create and verify the active 105 nginx routing backup');
|
||||
const goosedStart = source.indexOf('Start an isolated goosed candidate on 18015');
|
||||
const candidateStart = source.indexOf('Start the passive candidate Portal on 18081');
|
||||
const deepseekCompatStart = source.indexOf(
|
||||
'Start the DeepSeek tool-round compatibility proxy on 18036',
|
||||
);
|
||||
const proxyStart = source.indexOf('Start the fail-closed identity router on 18082');
|
||||
const tunnelStart = source.indexOf('Start the isolated 105 reverse tunnel on 19082');
|
||||
const edgeSwitch = source.indexOf(
|
||||
@@ -129,7 +145,8 @@ test('production canary keeps stable 8081 live and switches only after verified
|
||||
assert.ok(persistBackup > fullBackup);
|
||||
assert.ok(edgeBackup > persistBackup);
|
||||
assert.ok(goosedStart > edgeBackup);
|
||||
assert.ok(candidateStart > goosedStart);
|
||||
assert.ok(deepseekCompatStart > goosedStart);
|
||||
assert.ok(candidateStart > deepseekCompatStart);
|
||||
assert.ok(proxyStart > candidateStart);
|
||||
assert.ok(tunnelStart > proxyStart);
|
||||
assert.ok(edgeSwitch > tunnelStart);
|
||||
@@ -140,6 +157,12 @@ test('production canary keeps stable 8081 live and switches only after verified
|
||||
assert.match(source, /nginx -t/);
|
||||
assert.match(source, /CANARY_PROXY_PORT=18082/);
|
||||
assert.match(source, /CANARY_TUNNEL_REMOTE_PORT=19082/);
|
||||
assert.match(source, /DEEPSEEK_COMPAT_PORT=18036/);
|
||||
assert.match(source, /deepseek-no-think-proxy\.mjs/);
|
||||
assert.match(source, /run-deepseek-compat-proxy-candidate\.sh/);
|
||||
assert.match(source, /MEMIND_CANARY_CANDIDATE_HEALTH_URLS/);
|
||||
assert.match(source, /deepseek_compat_healthy/);
|
||||
assert.match(source, /bootout.*DEEPSEEK_COMPAT_LABEL/);
|
||||
assert.match(
|
||||
source,
|
||||
/edge_ssh\(\) \{\s*#.*\n\s*#.*\n\s*ssh -n -o BatchMode=yes/,
|
||||
@@ -176,6 +199,11 @@ test('candidate runner overrides stable host MCP paths with container-visible pa
|
||||
[
|
||||
'GOOSED_MCP_NODE_PATH=/opt/homebrew/opt/node@24/bin/node',
|
||||
'GOOSED_MCP_SERVER_PATH=/Users/john/Project/Memind/mindspace-sandbox-mcp.mjs',
|
||||
'MEMIND_DEEPSEEK_DISABLE_THINKING=0',
|
||||
'MEMIND_DEEPSEEK_NO_THINK_PORT=9999',
|
||||
'MEMIND_DEEPSEEK_NO_THINK_BASE_URL=http://wrong.invalid/v1',
|
||||
'MEMIND_DEEPSEEK_NO_THINK_HOST=wrong.invalid',
|
||||
'MEMIND_GOOSED_HOST_GATEWAY=wrong.invalid',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
@@ -186,6 +214,11 @@ test('candidate runner overrides stable host MCP paths with container-visible pa
|
||||
'printf "cwd=%s\\n" "$PWD"',
|
||||
'printf "mcp_node=%s\\n" "$GOOSED_MCP_NODE_PATH"',
|
||||
'printf "mcp_server=%s\\n" "$GOOSED_MCP_SERVER_PATH"',
|
||||
'printf "deepseek_disable=%s\\n" "$MEMIND_DEEPSEEK_DISABLE_THINKING"',
|
||||
'printf "deepseek_port=%s\\n" "$MEMIND_DEEPSEEK_NO_THINK_PORT"',
|
||||
'printf "deepseek_gateway=%s\\n" "$MEMIND_GOOSED_HOST_GATEWAY"',
|
||||
'printf "deepseek_base=%s\\n" "${MEMIND_DEEPSEEK_NO_THINK_BASE_URL-unset}"',
|
||||
'printf "deepseek_host=%s\\n" "${MEMIND_DEEPSEEK_NO_THINK_HOST-unset}"',
|
||||
'printf "entrypoint=%s\\n" "$1"',
|
||||
'',
|
||||
].join('\n'),
|
||||
@@ -215,9 +248,78 @@ test('candidate runner overrides stable host MCP paths with container-visible pa
|
||||
result.stdout,
|
||||
/^mcp_server=\/opt\/portal\/mindspace-sandbox-mcp\.mjs$/m,
|
||||
);
|
||||
assert.match(result.stdout, /^deepseek_disable=1$/m);
|
||||
assert.match(result.stdout, /^deepseek_port=18036$/m);
|
||||
assert.match(result.stdout, /^deepseek_gateway=host\.docker\.internal$/m);
|
||||
assert.match(result.stdout, /^deepseek_base=unset$/m);
|
||||
assert.match(result.stdout, /^deepseek_host=unset$/m);
|
||||
assert.match(result.stdout, new RegExp(`^entrypoint=${ROOT}/server\\.mjs$`, 'm'));
|
||||
});
|
||||
|
||||
test('canary rollback stops the DeepSeek compatibility process with the other canary services', async () => {
|
||||
const source = await fs.readFile(CANARY_ROLLBACK, 'utf8');
|
||||
assert.match(
|
||||
source,
|
||||
/DEEPSEEK_COMPAT_LABEL="cn\.tkmind\.memind-deepseek-compat-candidate"/,
|
||||
);
|
||||
assert.match(source, /bootout.*DEEPSEEK_COMPAT_LABEL/);
|
||||
assert.match(source, /docker rm -f goosed-prod-canary/);
|
||||
});
|
||||
|
||||
test('DeepSeek compatibility runner preserves stable upstream config but enforces candidate controls', async (t) => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-deepseek-runner-'));
|
||||
t.after(() => fs.rm(tempRoot, { recursive: true, force: true }));
|
||||
|
||||
const stableRoot = path.join(tempRoot, 'stable');
|
||||
const fakeNode = path.join(tempRoot, 'node');
|
||||
await fs.mkdir(stableRoot);
|
||||
await fs.writeFile(
|
||||
path.join(stableRoot, '.env'),
|
||||
[
|
||||
'DEEPSEEK_API_BASE_URL=https://synthetic-upstream.invalid/v1',
|
||||
'MEMIND_DEEPSEEK_PROXY_ENTRYPOINT=0',
|
||||
'MEMIND_DEEPSEEK_NO_THINK_PORT=9999',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
await fs.writeFile(
|
||||
fakeNode,
|
||||
[
|
||||
'#!/usr/bin/env bash',
|
||||
'printf "proxy_entrypoint=%s\\n" "$MEMIND_DEEPSEEK_PROXY_ENTRYPOINT"',
|
||||
'printf "proxy_port=%s\\n" "$MEMIND_DEEPSEEK_NO_THINK_PORT"',
|
||||
'printf "upstream=%s\\n" "$DEEPSEEK_API_BASE_URL"',
|
||||
'printf "entrypoint=%s\\n" "$1"',
|
||||
'',
|
||||
].join('\n'),
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
|
||||
const result = spawnSync(
|
||||
'bash',
|
||||
[path.join(ROOT, 'scripts', 'run-deepseek-compat-proxy-candidate.sh')],
|
||||
{
|
||||
cwd: '/',
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
MEMIND_CANARY_STABLE_ROOT: stableRoot,
|
||||
MEMIND_CANARY_DEEPSEEK_PROXY_PORT: '18036',
|
||||
NODE_BIN: fakeNode,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, /^proxy_entrypoint=1$/m);
|
||||
assert.match(result.stdout, /^proxy_port=18036$/m);
|
||||
assert.match(result.stdout, /^upstream=https:\/\/synthetic-upstream\.invalid\/v1$/m);
|
||||
assert.match(
|
||||
result.stdout,
|
||||
new RegExp(`^entrypoint=${ROOT}/deepseek-no-think-proxy\\.mjs$`, 'm'),
|
||||
);
|
||||
});
|
||||
|
||||
test('canary release and rollback remote shells remain syntactically valid', async () => {
|
||||
const releaseSource = await fs.readFile(CANARY_RELEASE, 'utf8');
|
||||
const rollbackSource = await fs.readFile(CANARY_ROLLBACK, 'utf8');
|
||||
|
||||
Reference in New Issue
Block a user