合并AI游戏创作智能体与通用Runtime基座
Project CI / Frontend tests (push) Failing after 27s
Project CI / Repository checks (push) Successful in 3m20s
Project CI / Backend tests (push) Successful in 3m52s
Project CI / Native shell tests (push) Successful in 13m19s

合并 codex/ai-game-creator-app 的独立客户端、多智能体 Runtime、Runner 与可扩展 LLM Provider 能力
保留 master 最新画布、钱包、后端和原生壳约束并完成四处语义化冲突合并
补齐根 Vitest 对独立 Tauri guest 模块的隔离测试别名并保持 HostBridge 依赖边界
统一正式工作台与 game-chat 的安全本地预览组件及 native-shell 静态门禁
修复合并态 Rust 格式、前端 lint 与共享文档验证口径
This commit is contained in:
AIGameCreator App
2026-07-30 18:01:19 +08:00
484 changed files with 383375 additions and 1517 deletions
File diff suppressed because it is too large Load Diff
+199 -70
View File
@@ -152,12 +152,14 @@ const SERVICE_ALIASES = new Map([
['bgfilterWorker', 'bgfilter-worker'],
['admin', 'admin-web'],
['adminWeb', 'admin-web'],
['backend', 'backend'],
['all', 'all'],
]);
function usage() {
console.log(`用法:
npm run dev [-- --watch] [-- --api-port 8090]
npm run dev backend [-- --watch]
npm run dev:spacetime [-- --skip-publish]
npm run dev:api-server [-- --database genarrative-dev]
npm run dev:bgfilter-worker [-- --database genarrative-dev]
@@ -388,7 +390,7 @@ function parseArgs(argv, baseEnv) {
function normalizeServiceName(rawName) {
const alias = SERVICE_ALIASES.get(rawName);
const name = alias ?? rawName;
if (name === 'all' || SERVICE_NAMES.includes(name)) {
if (name === 'all' || name === 'backend' || SERVICE_NAMES.includes(name)) {
return name;
}
@@ -410,10 +412,11 @@ function buildDevStackSnapshot(runner, updatedAt = new Date().toISOString()) {
}
return {
schemaVersion: 1,
schemaVersion: 2,
command: runner.command ?? 'all',
repoRoot,
database: runner.options.database,
spacetimeDataDir: resolve(runner.options.spacetimeDataDir),
watch: Boolean(runner.options.watch),
updatedAt,
services,
@@ -716,12 +719,13 @@ function ensureRequiredFiles(command) {
command === 'api-server' ||
command === 'bgfilter-worker' ||
command === 'spacetime' ||
command === 'all'
command === 'all' ||
command === 'backend'
) {
requiredFiles.push([manifestPath, 'server-rs/Cargo.toml']);
}
if (command === 'spacetime' || command === 'all') {
if (command === 'spacetime' || command === 'all' || command === 'backend') {
requiredFiles.push([
resolve(modulePath, 'Cargo.toml'),
'spacetime-module Cargo.toml',
@@ -1197,13 +1201,14 @@ class DevRunner {
if (
command === 'api-server' ||
command === 'bgfilter-worker' ||
command === 'all'
command === 'all' ||
command === 'backend'
) {
requireCommand('cargo');
}
if (
command === 'spacetime' ||
(command === 'all' &&
((command === 'all' || command === 'backend') &&
(!this.options.skipSpacetime || !this.options.skipPublish))
) {
requireCommand('spacetime');
@@ -1275,6 +1280,9 @@ class DevRunner {
if (command === 'all') {
return !this.options.skipSpacetime || !this.options.skipPublish;
}
if (command === 'backend') {
return !this.options.skipSpacetime || !this.options.skipPublish;
}
if (command === 'api-server' || command === 'bgfilter-worker') {
return isLoopbackSpacetimeServer(this.state.spacetimeServer);
}
@@ -1289,6 +1297,7 @@ class DevRunner {
if (
this.options.spacetimeServerUrl &&
command !== 'all' &&
command !== 'backend' &&
command !== 'spacetime'
) {
return;
@@ -1383,7 +1392,7 @@ class DevRunner {
const portRangeFor = (optionName) =>
this.explicitOptions.has(optionName) ? null : this.state.portRange;
if (command === 'all' || command === 'spacetime') {
if (command === 'all' || command === 'backend' || command === 'spacetime') {
if (!options.skipSpacetime && !this.state.spacetimeReused) {
portConfig.spacetime = {
host: options.spacetimeHost,
@@ -1393,7 +1402,11 @@ class DevRunner {
}
}
if (command === 'all' || command === 'api-server') {
if (
command === 'all' ||
command === 'backend' ||
command === 'api-server'
) {
portConfig.api = {
host: options.apiHost,
preferredPort: options.apiPort,
@@ -1469,7 +1482,7 @@ class DevRunner {
this.state.bgfilterWorkerTargetHost = resolveClientHost(
options.bgfilterWorkerHost,
);
if (command === 'all' || command === 'spacetime') {
if (command === 'all' || command === 'backend' || command === 'spacetime') {
this.state.spacetimeServer = `http://${options.spacetimeHost}:${options.spacetimePort}`;
}
this.state.apiTarget = `http://${this.state.apiTargetHost}:${options.apiPort}`;
@@ -1594,6 +1607,14 @@ class DevRunner {
return;
}
if (command === 'backend') {
await this.startSpacetimeForFullStack();
await this.services.get('api-server').start();
await this.waitForApiServer();
this.startWatchers(['spacetime', 'api-server']);
return;
}
if (command === 'spacetime') {
await this.startSpacetimeForFullStack();
} else {
@@ -1636,12 +1657,11 @@ class DevRunner {
await this.publishSpacetimeModule();
} catch (error) {
if (isSpacetimePublishPermissionError(error)) {
console.warn(
`[dev:spacetime] 本地发布被当前 identity 拒绝,保留已启动的 standalone: ${error.message}`,
throw new Error(
`本地数据库不属于当前隔离 identity,已停止启动以避免 API 使用旧 schema 后持续重试订阅。请改用独立本地数据目录,或在确认无需保留旧开发数据后重建该目录。详情: ${error.message}`,
);
} else {
throw error;
}
throw error;
}
}
}
@@ -1772,8 +1792,10 @@ class DevRunner {
async publishSpacetimeModule() {
const env = buildLocalRustProcessEnv(this.baseEnv);
this.prepareMigrationBootstrapSecret(env);
const cliConfigPath = await this.prepareLocalSpacetimeCliIdentity(env);
const args = buildSpacetimePublishArgs({
cliConfigPath,
database: this.options.database,
preserveDatabase: this.options.preserveDatabase,
server: this.state.spacetimeServer,
@@ -1787,6 +1809,48 @@ class DevRunner {
});
}
async prepareLocalSpacetimeCliIdentity(env) {
if (!isLoopbackSpacetimeServer(this.state.spacetimeServer)) {
return '';
}
await this.ensureApiServerSpacetimeToken();
const cliConfigPath = resolve(
this.options.spacetimeDataDir,
'dev-cli',
'cli.toml',
);
ensureParentDir(cliConfigPath);
if (
existsSync(cliConfigPath) &&
resolveCurrentSpacetimeCliToken(cliConfigPath) === this.spacetimeApiToken
) {
chmodSync(cliConfigPath, 0o600);
console.log('[dev:spacetime] 已复用隔离的本地发布 identity');
return cliConfigPath;
}
await runForeground(
'spacetime',
[
'--config-path',
cliConfigPath,
'login',
'--token',
this.spacetimeApiToken,
],
{
cwd: serverRsDir,
env,
label: 'spacetime-login',
},
);
if (existsSync(cliConfigPath)) {
chmodSync(cliConfigPath, 0o600);
}
console.log('[dev:spacetime] 已配置隔离的本地发布 identity');
return cliConfigPath;
}
prepareMigrationBootstrapSecret(env) {
let runtimeServiceBootstrapSecret = '';
switch (this.options.migrationBootstrapSecretMode) {
@@ -2098,8 +2162,8 @@ class DevRunner {
if (await isHttpReady(readinessUrl, 500)) {
return;
}
const runtimeStatus = this.services.get('bgfilter-worker')?.runtime
?.status;
const runtimeStatus =
this.services.get('bgfilter-worker')?.runtime?.status;
if (runtimeStatus === 'failed' || runtimeStatus === 'stopped') {
throw new Error(
`bgfilter-worker 在 readiness 前退出,请检查 logs/bgfilter-worker/: ${readinessUrl}`,
@@ -2718,10 +2782,85 @@ function normalizeSpacetimeServerForIdentity(serverUrl) {
return url.href.replace(/\/$/u, '');
}
function resolveLocalSpacetimeApiIdentityPath(dataDir, serverUrl) {
const normalizedServer = normalizeSpacetimeServerForIdentity(serverUrl);
const serverKey = createHash('sha256').update(normalizedServer).digest('hex');
return resolve(dataDir, 'dev-api-identities', `${serverKey}.json`);
function resolveLocalSpacetimeApiIdentityPath(dataDir) {
return resolve(dataDir, 'dev-api-identities', 'local-node.json');
}
function readLocalSpacetimeApiIdentityRecord(identityPath, expected = {}) {
const stat = lstatSync(identityPath);
if (!stat.isFile() || stat.isSymbolicLink()) {
throw new Error('记录不是普通文件');
}
chmodSync(identityPath, 0o600);
const payload = JSON.parse(readFileSync(identityPath, 'utf8'));
const identity =
typeof payload.identity === 'string' ? payload.identity.trim() : '';
const token = typeof payload.token === 'string' ? payload.token.trim() : '';
if (!identity || !token) {
throw new Error('记录缺少 identity 或 token');
}
if (payload.schemaVersion === 2 && payload.scope === 'local-data-dir') {
return { identity, token };
}
if (
expected.allowLegacy &&
payload.schemaVersion === 1 &&
typeof payload.server === 'string' &&
isLoopbackSpacetimeServer(payload.server)
) {
return { identity, token };
}
throw new Error('记录格式或 data dir 作用域不匹配');
}
function migrateLegacyLocalSpacetimeApiIdentity(dataDir) {
const identityDir = resolve(dataDir, 'dev-api-identities');
if (!existsSync(identityDir)) {
return null;
}
const candidates = [];
for (const entry of readdirSync(identityDir, { withFileTypes: true })) {
if (!entry.isFile() || !entry.name.endsWith('.json')) {
continue;
}
const candidatePath = resolve(identityDir, entry.name);
if (candidatePath === resolveLocalSpacetimeApiIdentityPath(dataDir)) {
continue;
}
try {
candidates.push(
readLocalSpacetimeApiIdentityRecord(candidatePath, {
allowLegacy: true,
}),
);
} catch {
// 无效或非本地旧记录不参与迁移。
}
}
const uniqueCandidates = new Map(
candidates.map((candidate) => [
`${candidate.identity}\n${candidate.token}`,
candidate,
]),
);
if (uniqueCandidates.size === 0) {
return null;
}
if (uniqueCandidates.size > 1) {
throw new Error(
'同一 SpacetimeDB data dir 下发现多个旧 API identity,无法安全判断数据库 owner;请保留正确 owner 记录后重试',
);
}
const [identity] = uniqueCandidates.values();
writeLocalSpacetimeApiIdentity({ dataDir, ...identity });
console.log(
'[dev:spacetime] 已将旧端口作用域 API identity 迁移到 data dir 作用域',
);
return identity;
}
function resolveLocalSpacetimeRuntimeServiceBootstrapSecretPath(
@@ -2871,37 +3010,13 @@ function readLocalSpacetimeApiIdentity({ dataDir, serverUrl }) {
return null;
}
const normalizedServer = normalizeSpacetimeServerForIdentity(serverUrl);
const identityPath = resolveLocalSpacetimeApiIdentityPath(
dataDir,
normalizedServer,
);
const identityPath = resolveLocalSpacetimeApiIdentityPath(dataDir);
if (!existsSync(identityPath)) {
return null;
return migrateLegacyLocalSpacetimeApiIdentity(dataDir);
}
try {
const stat = lstatSync(identityPath);
if (!stat.isFile() || stat.isSymbolicLink()) {
throw new Error('记录不是普通文件');
}
chmodSync(identityPath, 0o600);
const payload = JSON.parse(readFileSync(identityPath, 'utf8'));
if (
payload.schemaVersion !== 1 ||
payload.server !== normalizedServer ||
typeof payload.identity !== 'string' ||
!payload.identity.trim() ||
typeof payload.token !== 'string' ||
!payload.token.trim()
) {
throw new Error('记录格式或 server 绑定不匹配');
}
return {
identity: payload.identity.trim(),
token: payload.token.trim(),
};
return readLocalSpacetimeApiIdentityRecord(identityPath);
} catch (error) {
console.warn(
`[dev:spacetime] 本地 API identity 记录不可用,将重新创建: ${error.message}`,
@@ -2910,17 +3025,8 @@ function readLocalSpacetimeApiIdentity({ dataDir, serverUrl }) {
}
}
function writeLocalSpacetimeApiIdentity({
dataDir,
serverUrl,
identity,
token,
}) {
const normalizedServer = normalizeSpacetimeServerForIdentity(serverUrl);
const identityPath = resolveLocalSpacetimeApiIdentityPath(
dataDir,
normalizedServer,
);
function writeLocalSpacetimeApiIdentity({ dataDir, identity, token }) {
const identityPath = resolveLocalSpacetimeApiIdentityPath(dataDir);
const tempPath = `${identityPath}.${process.pid}.${randomHex(8)}.tmp`;
ensureParentDir(identityPath);
@@ -2928,8 +3034,8 @@ function writeLocalSpacetimeApiIdentity({
writeFileSync(
tempPath,
`${JSON.stringify({
schemaVersion: 1,
server: normalizedServer,
schemaVersion: 2,
scope: 'local-data-dir',
identity,
token,
})}\n`,
@@ -3089,8 +3195,14 @@ function isLoopbackSpacetimeServer(serverUrl) {
}
}
function resolveCurrentSpacetimeCliToken() {
const result = spawnSync('spacetime', ['login', 'show', '--token'], {
function resolveCurrentSpacetimeCliToken(cliConfigPath = '') {
const args = [
...(cliConfigPath ? ['--config-path', cliConfigPath] : []),
'login',
'show',
'--token',
];
const result = spawnSync('spacetime', args, {
cwd: repoRoot,
encoding: 'utf8',
shell: process.platform === 'win32',
@@ -3114,13 +3226,21 @@ function trimPreview(text, maxLength = 300) {
function runForeground(command, args, { cwd, env, label }) {
return new Promise((resolveRun, rejectRun) => {
let capturedOutput = '';
const capture = (chunk, target) => {
target.write(chunk);
capturedOutput = `${capturedOutput}${String(chunk)}`.slice(-32_768);
};
const child = spawn(command, args, {
cwd,
env,
stdio: 'inherit',
stdio: ['inherit', 'pipe', 'pipe'],
shell: process.platform === 'win32',
});
child.stdout?.on('data', (chunk) => capture(chunk, process.stdout));
child.stderr?.on('data', (chunk) => capture(chunk, process.stderr));
child.on('error', rejectRun);
child.on('exit', (code, signal) => {
if (signal) {
@@ -3129,7 +3249,12 @@ function runForeground(command, args, { cwd, env, label }) {
}
if (code !== 0) {
rejectRun(new Error(`[dev:${label}] 退出码: ${code}`));
const detail = trimPreview(capturedOutput, 2_000);
rejectRun(
new Error(
`[dev:${label}] 退出码: ${code}${detail ? `: ${detail}` : ''}`,
),
);
return;
}
@@ -3189,8 +3314,14 @@ function isDirectModuleExecution(argv1, moduleUrl, resolvePath = safeRealpath) {
}
}
function buildSpacetimePublishArgs({ database, server, preserveDatabase }) {
function buildSpacetimePublishArgs({
cliConfigPath = '',
database,
server,
preserveDatabase,
}) {
const args = [
...(cliConfigPath ? ['--config-path', cliConfigPath] : []),
'publish',
database,
'--server',
@@ -3260,17 +3391,15 @@ function buildBgfilterWorkerProcessEnv({
GENARRATIVE_BGFILTER_WORKER_BASE_URL: state.bgfilterWorkerTarget,
GENARRATIVE_BGFILTER_INTERNAL_TOKEN: bgfilterInternalToken,
GENARRATIVE_BGFILTER_WORKER_CONCURRENCY:
String(
baseEnv.GENARRATIVE_BGFILTER_WORKER_CONCURRENCY ?? '',
).trim() || '16',
String(baseEnv.GENARRATIVE_BGFILTER_WORKER_CONCURRENCY ?? '').trim() ||
'16',
GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS:
String(
baseEnv.GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS ?? '',
).trim() || '5000',
GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS:
String(
baseEnv.GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS ?? '',
).trim() || '2048',
String(baseEnv.GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS ?? '').trim() ||
'2048',
GENARRATIVE_API_LOG: options.apiLog,
GENARRATIVE_SPACETIME_SERVER_URL: state.spacetimeServer,
GENARRATIVE_SPACETIME_DATABASE: options.database,
+117 -25
View File
@@ -11,7 +11,7 @@ import {
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { dirname, join, resolve } from 'node:path';
import { afterEach, describe, expect, test, vi } from 'vitest';
@@ -673,9 +673,12 @@ describe('dev scheduler stack state file', () => {
const snapshot = buildDevStackSnapshot(runner, updatedAt);
expect(snapshot.schemaVersion).toBe(1);
expect(snapshot.schemaVersion).toBe(2);
expect(snapshot.command).toBe('web');
expect(snapshot.database).toBe('genarrative-test');
expect(snapshot.spacetimeDataDir).toBe(
resolve('server-rs/.spacetimedb/local/data'),
);
expect(snapshot.services.web).toMatchObject({
status: 'running',
pid: 4321,
@@ -974,16 +977,20 @@ spacetimedb tool version 2.7.0; spacetimedb-lib version 2.7.0;
).toBe(false);
});
test('发布 spacetime-module 时忽略 spacetime.json 以免覆盖显式数据库', () => {
test('发布 spacetime-module 时使用隔离身份配置并忽略 spacetime.json', () => {
const args = buildSpacetimePublishArgs({
cliConfigPath: '/tmp/genarrative-cli.toml',
database: 'xushi-p4wfr',
preserveDatabase: false,
server: 'http://127.0.0.1:3101',
});
expect(args).toContain('--no-config');
expect(args).not.toContain('--anonymous');
expect(args).toEqual(
expect.arrayContaining([
'--config-path',
'/tmp/genarrative-cli.toml',
'publish',
'xushi-p4wfr',
'--server',
@@ -993,6 +1000,17 @@ spacetimedb tool version 2.7.0; spacetimedb-lib version 2.7.0;
);
});
test('远程 SpacetimeDB 发布继续使用默认登录身份', () => {
const args = buildSpacetimePublishArgs({
database: 'xushi-p4wfr',
preserveDatabase: true,
server: 'https://spacetime.example.com',
});
expect(args).not.toContain('--anonymous');
expect(args).not.toContain('--config-path');
});
test('手动刷新 spacetime 只重新发布模块,不重启 standalone 进程', async () => {
const { explicitOptions, options } = parseArgs([], {});
const runner = new DevRunner(options, {}, explicitOptions);
@@ -1025,26 +1043,18 @@ spacetimedb tool version 2.7.0; spacetimedb-lib version 2.7.0;
expect(runner.publishSpacetimeModule).not.toHaveBeenCalled();
});
test('本地 API identity 路径同时绑定 data dir 和规范化 server', () => {
test('本地 API identity 路径绑定 data dir', () => {
const first = resolveLocalSpacetimeApiIdentityPath(
'/tmp/genarrative-data-a',
'http://127.0.0.1:3101',
);
const normalizedEquivalent = resolveLocalSpacetimeApiIdentityPath(
const sameDataDir = resolveLocalSpacetimeApiIdentityPath(
'/tmp/genarrative-data-a',
'http://127.0.0.1:3101/',
);
const otherServer = resolveLocalSpacetimeApiIdentityPath(
'/tmp/genarrative-data-a',
'http://127.0.0.1:3102',
);
const otherDataDir = resolveLocalSpacetimeApiIdentityPath(
'/tmp/genarrative-data-b',
'http://127.0.0.1:3101',
);
expect(normalizedEquivalent).toBe(first);
expect(otherServer).not.toBe(first);
expect(sameDataDir).toBe(first);
expect(otherDataDir).not.toBe(first);
expect(first).toContain(join('genarrative-data-a', 'dev-api-identities'));
});
@@ -1071,10 +1081,7 @@ spacetimedb tool version 2.7.0; spacetimedb-lib version 2.7.0;
await firstRunner.ensureApiServerSpacetimeToken();
const identityPath = resolveLocalSpacetimeApiIdentityPath(
tempDir,
firstRunner.state.spacetimeServer,
);
const identityPath = resolveLocalSpacetimeApiIdentityPath(tempDir);
expect(firstRunner.spacetimeApiToken).toBe('local-api-token');
expect(firstRunner.baseEnv.GENARRATIVE_SPACETIME_TOKEN).toBeUndefined();
expect(globalThis.fetch).toHaveBeenCalledWith(
@@ -1082,8 +1089,8 @@ spacetimedb tool version 2.7.0; spacetimedb-lib version 2.7.0;
expect.objectContaining({ method: 'POST' }),
);
expect(JSON.parse(readFileSync(identityPath, 'utf8'))).toMatchObject({
schemaVersion: 1,
server: 'http://127.0.0.1:3101',
schemaVersion: 2,
scope: 'local-data-dir',
identity: 'c200localidentity',
token: 'local-api-token',
});
@@ -1093,7 +1100,7 @@ spacetimedb tool version 2.7.0; spacetimedb-lib version 2.7.0;
}
const secondRunner = new DevRunner(options, {}, explicitOptions);
secondRunner.state.spacetimeServer = 'http://127.0.0.1:3101';
secondRunner.state.spacetimeServer = 'http://127.0.0.1:3199';
globalThis.fetch = vi.fn();
await secondRunner.ensureApiServerSpacetimeToken();
@@ -1112,6 +1119,94 @@ spacetimedb tool version 2.7.0; spacetimedb-lib version 2.7.0;
}
});
test('旧端口作用域 API identity 会迁移为 data dir 作用域', async () => {
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-api-identity-'));
try {
const legacyServer = 'http://127.0.0.1:3101';
const legacyKey = createHash('sha256').update(legacyServer).digest('hex');
const legacyPath = join(
tempDir,
'dev-api-identities',
`${legacyKey}.json`,
);
mkdirSync(dirname(legacyPath), { recursive: true });
writeFileSync(
legacyPath,
`${JSON.stringify({
schemaVersion: 1,
server: legacyServer,
identity: 'legacy-owner-identity',
token: 'legacy-owner-token',
})}\n`,
{ mode: 0o600 },
);
const { explicitOptions, options } = parseArgs(
['--spacetime-data-dir', tempDir],
{},
);
const runner = new DevRunner(options, {}, explicitOptions);
runner.state.spacetimeServer = 'http://127.0.0.1:3199';
globalThis.fetch = vi.fn();
await runner.ensureApiServerSpacetimeToken();
expect(runner.spacetimeApiToken).toBe('legacy-owner-token');
expect(globalThis.fetch).not.toHaveBeenCalled();
expect(
JSON.parse(
readFileSync(resolveLocalSpacetimeApiIdentityPath(tempDir), 'utf8'),
),
).toMatchObject({
schemaVersion: 2,
scope: 'local-data-dir',
identity: 'legacy-owner-identity',
});
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
test('同一 data dir 存在多个旧 identity 时失败关闭', async () => {
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-api-identity-'));
try {
for (const [port, identity] of [
[3101, 'legacy-owner-a'],
[3199, 'legacy-owner-b'],
] as const) {
const server = `http://127.0.0.1:${port}`;
const legacyPath = join(
tempDir,
'dev-api-identities',
`${createHash('sha256').update(server).digest('hex')}.json`,
);
mkdirSync(dirname(legacyPath), { recursive: true });
writeFileSync(
legacyPath,
`${JSON.stringify({
schemaVersion: 1,
server,
identity,
token: `${identity}-token`,
})}\n`,
{ mode: 0o600 },
);
}
const { explicitOptions, options } = parseArgs(
['--spacetime-data-dir', tempDir],
{},
);
const runner = new DevRunner(options, {}, explicitOptions);
globalThis.fetch = vi.fn();
await expect(runner.ensureApiServerSpacetimeToken()).rejects.toThrow(
'无法安全判断数据库 owner',
);
expect(globalThis.fetch).not.toHaveBeenCalled();
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
test('外部显式 token 优先于已持久化的本地 API identity', async () => {
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-api-identity-'));
const originalToken = process.env.GENARRATIVE_SPACETIME_TOKEN;
@@ -1167,10 +1262,7 @@ spacetimedb tool version 2.7.0; spacetimedb-lib version 2.7.0;
);
const runner = new DevRunner(options, {}, explicitOptions);
runner.state.spacetimeServer = 'http://127.0.0.1:3101';
const identityPath = resolveLocalSpacetimeApiIdentityPath(
tempDir,
runner.state.spacetimeServer,
);
const identityPath = resolveLocalSpacetimeApiIdentityPath(tempDir);
mkdirSync(dirname(identityPath), { recursive: true });
const linkedRecordPath = join(tempDir, 'linked-api-identity.json');
writeFileSync(