完成 Editor Agent Mock Agent P1 收尾
接入 Web Project 契约、SpacetimeDB 表与 api-server 控制面 新增 Mock Agent、静态构建 runner 与独立预览网关 补齐 /editor/agent 前端页面、服务客户端和 SSE 订阅 修复 sandbox 预览资源跨域加载并补充并发保护 接入本地 dev 预览端口漂移与服务身份初始化 更新 P1 技术方案、验收清单和 Hermes 共享记忆
This commit is contained in:
@@ -1,24 +1,29 @@
|
||||
import {mkdtempSync, readFileSync, rmSync, writeFileSync} from 'node:fs';
|
||||
import {tmpdir} from 'node:os';
|
||||
import {join} from 'node:path';
|
||||
import {join, resolve} from 'node:path';
|
||||
|
||||
import {afterEach, describe, expect, test, vi} from 'vitest';
|
||||
|
||||
import {
|
||||
assertReusableSpacetimeProcessVersionMatchesWorkspace,
|
||||
assertSpacetimeToolVersionMatchesWorkspace,
|
||||
authorizeWebProjectServiceIdentity,
|
||||
buildApiServerProcessEnv,
|
||||
buildDevStackSnapshot,
|
||||
buildLocalRustProcessEnv,
|
||||
buildSpacetimeProcedureUrl,
|
||||
buildSpacetimePublishArgs,
|
||||
cacheSpacetimeIdentity,
|
||||
createDevServerSpawnOptions,
|
||||
createWatchConfigs,
|
||||
DevRunner,
|
||||
isDirectModuleExecution,
|
||||
isSpacetimePublishPermissionError,
|
||||
normalizeCargoVersionRequirement,
|
||||
parseWebProjectServiceIdentityResult,
|
||||
parseArgs,
|
||||
parseSpacetimeToolVersion,
|
||||
readCachedSpacetimeIdentity,
|
||||
resolveDevStackStatePath,
|
||||
shouldAcceptWatchEvent,
|
||||
} from './dev.mjs';
|
||||
@@ -109,7 +114,7 @@ describe('dev scheduler argument routing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
linuxTest('Linux 启动时按系统级端口段映射四个 dev 端口', async () => {
|
||||
linuxTest('Linux 启动时按系统级端口段映射 dev 端口和预览网关端口', async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-dev-port-range-'));
|
||||
try {
|
||||
const {command, explicitOptions, options} = parseArgs([], {
|
||||
@@ -132,8 +137,10 @@ describe('dev scheduler argument routing', () => {
|
||||
expect(runner.options.apiPort).toBe(22001);
|
||||
expect(runner.options.spacetimePort).toBe(22002);
|
||||
expect(runner.options.adminWebPort).toBe(22003);
|
||||
expect(runner.options.webProjectPreviewPort).toBe(22004);
|
||||
expect(runner.state.apiTarget).toBe('http://127.0.0.1:22001');
|
||||
expect(runner.state.spacetimeServer).toBe('http://127.0.0.1:22002');
|
||||
expect(runner.state.webProjectPreviewPublicBaseUrl).toBe('http://127.0.0.1:22004');
|
||||
} finally {
|
||||
rmSync(tempDir, {recursive: true, force: true});
|
||||
}
|
||||
@@ -163,6 +170,7 @@ describe('dev scheduler argument routing', () => {
|
||||
expect(runner.options.apiPort).toBe(8082);
|
||||
expect(runner.options.spacetimePort).toBe(3101);
|
||||
expect(runner.options.adminWebPort).toBe(3102);
|
||||
expect(runner.options.webProjectPreviewPort).toBe(3104);
|
||||
} finally {
|
||||
if (originalPlatform) {
|
||||
Object.defineProperty(process, 'platform', originalPlatform);
|
||||
@@ -183,6 +191,42 @@ describe('dev scheduler api-server env', () => {
|
||||
expect(env.GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED).toBe('true');
|
||||
expect(env.GENARRATIVE_API_PORT).toBe('9091');
|
||||
expect(env.GENARRATIVE_SPACETIME_SERVER_URL).toBe('http://127.0.0.1:3199');
|
||||
expect(env.GENARRATIVE_WEB_PROJECT_PREVIEW_HOST).toBe('127.0.0.1');
|
||||
expect(env.GENARRATIVE_WEB_PROJECT_PREVIEW_PORT).toBe('3104');
|
||||
expect(env.GENARRATIVE_WEB_PROJECT_PREVIEW_PUBLIC_BASE_URL).toBe('http://127.0.0.1:3104');
|
||||
expect(env.GENARRATIVE_WEB_PROJECT_PREVIEW_FRAME_ANCESTORS).toBe(
|
||||
'http://127.0.0.1:3000 http://localhost:3000',
|
||||
);
|
||||
});
|
||||
|
||||
test('dev 脚本默认注入 Web Project runner 二进制路径', () => {
|
||||
const {options} = parseArgs(['api-server'], {});
|
||||
const env = buildApiServerProcessEnv({
|
||||
baseEnv: {},
|
||||
options,
|
||||
state: {spacetimeServer: 'http://127.0.0.1:3101'},
|
||||
});
|
||||
const runnerName =
|
||||
process.platform === 'win32' ? 'web-project-runner.exe' : 'web-project-runner';
|
||||
|
||||
expect(env.GENARRATIVE_WEB_PROJECT_RUNNER_BIN).toBe(
|
||||
resolve('server-rs', 'target/debug', runnerName),
|
||||
);
|
||||
});
|
||||
|
||||
test('dev 脚本保留显式 Web Project runner 二进制路径', () => {
|
||||
const {options} = parseArgs(['api-server'], {});
|
||||
const env = buildApiServerProcessEnv({
|
||||
baseEnv: {
|
||||
GENARRATIVE_WEB_PROJECT_RUNNER_BIN: 'C:\\tools\\web-project-runner.exe',
|
||||
},
|
||||
options,
|
||||
state: {spacetimeServer: 'http://127.0.0.1:3101'},
|
||||
});
|
||||
|
||||
expect(env.GENARRATIVE_WEB_PROJECT_RUNNER_BIN).toBe(
|
||||
'C:\\tools\\web-project-runner.exe',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -199,6 +243,7 @@ describe('dev scheduler Rust build env', () => {
|
||||
expect(env.RUSTC_WRAPPER).not.toBe('/usr/bin/sccache');
|
||||
expect(env.RUSTC_WRAPPER).not.toBe('sccache');
|
||||
expect(env.CARGO_BUILD_RUSTC_WRAPPER).toBe(env.RUSTC_WRAPPER);
|
||||
expect(env.RUSTC_WRAPPER).toBe(process.platform === 'win32' ? '' : '/usr/bin/env');
|
||||
});
|
||||
|
||||
test('local dev Rust env keeps healthy custom wrapper untouched', () => {
|
||||
@@ -233,6 +278,8 @@ describe('dev scheduler stack state file', () => {
|
||||
webPort: 3010,
|
||||
adminWebHost: '127.0.0.1',
|
||||
adminWebPort: 3110,
|
||||
webProjectPreviewHost: '127.0.0.1',
|
||||
webProjectPreviewPort: 3112,
|
||||
spacetimeHost: '127.0.0.1',
|
||||
spacetimePort: 3120,
|
||||
spacetimeDataDir: 'server-rs/.spacetimedb/local/data',
|
||||
@@ -243,6 +290,7 @@ describe('dev scheduler stack state file', () => {
|
||||
apiTarget: 'http://127.0.0.1:8090',
|
||||
adminWebTargetHost: '127.0.0.1',
|
||||
spacetimeServer: 'http://127.0.0.1:3120',
|
||||
webProjectPreviewPublicBaseUrl: 'http://127.0.0.1:3112',
|
||||
},
|
||||
services: new Map([
|
||||
[
|
||||
@@ -285,6 +333,14 @@ describe('dev scheduler stack state file', () => {
|
||||
port: 8090,
|
||||
url: 'http://127.0.0.1:8090',
|
||||
});
|
||||
expect(snapshot.services['web-project-preview']).toMatchObject({
|
||||
status: 'idle',
|
||||
pid: null,
|
||||
host: '127.0.0.1',
|
||||
port: 3112,
|
||||
url: 'http://127.0.0.1:3112',
|
||||
command: 'api-server embedded Web Project preview gateway',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -500,6 +556,85 @@ spacetimedb tool version 2.5.0; spacetimedb-lib version 2.5.0;
|
||||
);
|
||||
});
|
||||
|
||||
test('发布 spacetime-module 时准备 Web Project 服务身份引导密钥', () => {
|
||||
const {explicitOptions, options} = parseArgs([], {});
|
||||
const runner = new DevRunner(options, {}, explicitOptions);
|
||||
const env = {};
|
||||
|
||||
runner.prepareWebProjectServiceBootstrapSecret(env);
|
||||
|
||||
expect(env.GENARRATIVE_SPACETIME_WEB_PROJECT_SERVICE_BOOTSTRAP_SECRET).toHaveLength(64);
|
||||
expect(runner.state.webProjectServiceBootstrapSecretPrepared).toBe(true);
|
||||
});
|
||||
|
||||
test('Web Project 服务身份授权使用本地 Web token 调用 procedure', async () => {
|
||||
globalThis.fetch = vi.fn(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () =>
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
service_identity_hex: 'c200localidentity',
|
||||
error_message: null,
|
||||
}),
|
||||
})) as unknown as typeof fetch;
|
||||
|
||||
await authorizeWebProjectServiceIdentity({
|
||||
serverUrl: 'http://127.0.0.1:3101',
|
||||
database: 'genarrative-dev',
|
||||
token: 'local-web-token',
|
||||
bootstrapSecret: '0123456789abcdef',
|
||||
serviceIdentity: 'c200localidentity',
|
||||
});
|
||||
|
||||
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||||
buildSpacetimeProcedureUrl(
|
||||
'http://127.0.0.1:3101',
|
||||
'genarrative-dev',
|
||||
'authorize_web_project_service_identity',
|
||||
),
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer local-web-token',
|
||||
}),
|
||||
body: JSON.stringify([
|
||||
{
|
||||
bootstrap_secret: '0123456789abcdef',
|
||||
service_identity_hex: 'c200localidentity',
|
||||
note: 'local api-server web project',
|
||||
},
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('Web Project 服务身份授权返回值兼容 BSATN 数组形态', () => {
|
||||
expect(parseWebProjectServiceIdentityResult('[true,[0,"c200localidentity"],[1]]')).toEqual({
|
||||
ok: true,
|
||||
service_identity_hex: 'c200localidentity',
|
||||
error_message: null,
|
||||
});
|
||||
});
|
||||
|
||||
test('本地 api-server SpacetimeDB identity 写入并复用 data-dir 缓存', () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-api-identity-'));
|
||||
try {
|
||||
cacheSpacetimeIdentity(tempDir, 'http://127.0.0.1:3101', {
|
||||
identity: 'c200cachedidentity',
|
||||
token: 'cached-token',
|
||||
});
|
||||
|
||||
expect(readCachedSpacetimeIdentity(tempDir, 'http://127.0.0.1:3101')).toEqual({
|
||||
identity: 'c200cachedidentity',
|
||||
token: 'cached-token',
|
||||
});
|
||||
expect(readCachedSpacetimeIdentity(tempDir, 'http://127.0.0.1:3199')).toBeNull();
|
||||
} finally {
|
||||
rmSync(tempDir, {recursive: true, force: true});
|
||||
}
|
||||
});
|
||||
|
||||
test('手动刷新 spacetime 只重新发布模块,不重启 standalone 进程', async () => {
|
||||
const {explicitOptions, options} = parseArgs([], {});
|
||||
const runner = new DevRunner(options, {}, explicitOptions);
|
||||
@@ -533,39 +668,55 @@ spacetimedb tool version 2.5.0; spacetimedb-lib version 2.5.0;
|
||||
});
|
||||
|
||||
test('启动 api-server 前为空 token 自动创建本地 Web identity', async () => {
|
||||
const {explicitOptions, options} = parseArgs([], {
|
||||
GENARRATIVE_SPACETIME_TOKEN: '',
|
||||
});
|
||||
const runner = new DevRunner(options, {}, explicitOptions);
|
||||
runner.state.spacetimeServer = 'http://127.0.0.1:3101';
|
||||
globalThis.fetch = vi.fn(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () =>
|
||||
JSON.stringify({
|
||||
identity: 'c200localidentity',
|
||||
token: 'local-web-token',
|
||||
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-api-identity-'));
|
||||
try {
|
||||
const {explicitOptions, options} = parseArgs(
|
||||
['--spacetime-data-dir', tempDir],
|
||||
{
|
||||
GENARRATIVE_SPACETIME_TOKEN: '',
|
||||
},
|
||||
);
|
||||
const runner = new DevRunner(options, {}, explicitOptions);
|
||||
runner.state.spacetimeServer = 'http://127.0.0.1:3101';
|
||||
globalThis.fetch = vi.fn(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () =>
|
||||
JSON.stringify({
|
||||
identity: 'c200localidentity',
|
||||
token: 'local-web-token',
|
||||
}),
|
||||
})) as unknown as typeof fetch;
|
||||
|
||||
await runner.ensureApiServerSpacetimeToken();
|
||||
|
||||
expect(runner.baseEnv.GENARRATIVE_SPACETIME_TOKEN).toBe('local-web-token');
|
||||
expect(readCachedSpacetimeIdentity(tempDir, 'http://127.0.0.1:3101')).toEqual({
|
||||
identity: 'c200localidentity',
|
||||
token: 'local-web-token',
|
||||
});
|
||||
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||||
'http://127.0.0.1:3101/v1/identity',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
}),
|
||||
})) as unknown as typeof fetch;
|
||||
|
||||
await runner.ensureApiServerSpacetimeToken();
|
||||
|
||||
expect(runner.baseEnv.GENARRATIVE_SPACETIME_TOKEN).toBe('local-web-token');
|
||||
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||||
'http://127.0.0.1:3101/v1/identity',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
}),
|
||||
);
|
||||
);
|
||||
} finally {
|
||||
rmSync(tempDir, {recursive: true, force: true});
|
||||
}
|
||||
});
|
||||
|
||||
test('本地 SpacetimeDB 不信任 env 文件中的陈旧 token', async () => {
|
||||
const originalToken = process.env.GENARRATIVE_SPACETIME_TOKEN;
|
||||
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-api-identity-'));
|
||||
delete process.env.GENARRATIVE_SPACETIME_TOKEN;
|
||||
try {
|
||||
const {explicitOptions, options} = parseArgs([], {
|
||||
GENARRATIVE_SPACETIME_TOKEN: 'stale-env-file-token',
|
||||
});
|
||||
const {explicitOptions, options} = parseArgs(
|
||||
['--spacetime-data-dir', tempDir],
|
||||
{
|
||||
GENARRATIVE_SPACETIME_TOKEN: 'stale-env-file-token',
|
||||
},
|
||||
);
|
||||
const runner = new DevRunner(options, {GENARRATIVE_SPACETIME_TOKEN: 'stale-env-file-token'}, explicitOptions);
|
||||
runner.state.spacetimeServer = 'http://127.0.0.1:3101';
|
||||
globalThis.fetch = vi.fn(async () => ({
|
||||
@@ -582,6 +733,7 @@ spacetimedb tool version 2.5.0; spacetimedb-lib version 2.5.0;
|
||||
|
||||
expect(runner.baseEnv.GENARRATIVE_SPACETIME_TOKEN).toBe('fresh-web-token');
|
||||
} finally {
|
||||
rmSync(tempDir, {recursive: true, force: true});
|
||||
if (originalToken === undefined) {
|
||||
delete process.env.GENARRATIVE_SPACETIME_TOKEN;
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user