修复AI游戏创作壳跨平台启动 (#107)
修复 macOS 下 Unix 文件身份比较和临时目录测试兼容 隔离 AI 游戏创作本地数据库与发布身份并阻止旧 schema 降级启动 完善 Tauri 开发栈错误传播和 POSIX 子进程树清理 跳过 macOS 不支持的进程指标回调以消除周期告警 补充开发调度测试、技术方案和团队排障记忆 Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/107 Co-authored-by: menghao <mh18530625731@163.com> Co-committed-by: menghao <mh18530625731@163.com>
This commit was merged in pull request #107.
This commit is contained in:
+176
-57
@@ -383,10 +383,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,
|
||||
@@ -924,7 +925,7 @@ function readLinuxApiServerProcessSnapshot(pid) {
|
||||
if (
|
||||
error?.code === 'ENOENT' ||
|
||||
error?.code === 'EACCES' ||
|
||||
error?.code === 'EPERM' ||
|
||||
error?.code === 'EPERM' ||
|
||||
error?.code === 'ESRCH'
|
||||
) {
|
||||
return null;
|
||||
@@ -1124,7 +1125,11 @@ class DevRunner {
|
||||
this.command = command;
|
||||
ensureRequiredFiles(command);
|
||||
requireCommand('node');
|
||||
if (command === 'api-server' || command === 'all' || command === 'backend') {
|
||||
if (
|
||||
command === 'api-server' ||
|
||||
command === 'all' ||
|
||||
command === 'backend'
|
||||
) {
|
||||
requireCommand('cargo');
|
||||
}
|
||||
if (
|
||||
@@ -1319,7 +1324,11 @@ class DevRunner {
|
||||
}
|
||||
}
|
||||
|
||||
if (command === 'all' || command === 'backend' || command === 'api-server') {
|
||||
if (
|
||||
command === 'all' ||
|
||||
command === 'backend' ||
|
||||
command === 'api-server'
|
||||
) {
|
||||
portConfig.api = {
|
||||
host: options.apiHost,
|
||||
preferredPort: options.apiPort,
|
||||
@@ -1513,12 +1522,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1645,8 +1653,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,
|
||||
@@ -1660,6 +1670,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) {
|
||||
@@ -2447,10 +2499,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(
|
||||
@@ -2600,37 +2727,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}`,
|
||||
@@ -2639,17 +2742,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);
|
||||
|
||||
@@ -2657,8 +2751,8 @@ function writeLocalSpacetimeApiIdentity({
|
||||
writeFileSync(
|
||||
tempPath,
|
||||
`${JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
server: normalizedServer,
|
||||
schemaVersion: 2,
|
||||
scope: 'local-data-dir',
|
||||
identity,
|
||||
token,
|
||||
})}\n`,
|
||||
@@ -2814,8 +2908,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',
|
||||
@@ -2839,13 +2939,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) {
|
||||
@@ -2854,7 +2962,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;
|
||||
}
|
||||
|
||||
@@ -2914,8 +3027,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',
|
||||
|
||||
+117
-25
@@ -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';
|
||||
|
||||
@@ -510,9 +510,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,
|
||||
@@ -761,16 +764,20 @@ spacetimedb tool version 2.6.0; spacetimedb-lib version 2.6.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',
|
||||
@@ -780,6 +787,17 @@ spacetimedb tool version 2.6.0; spacetimedb-lib version 2.6.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);
|
||||
@@ -812,26 +830,18 @@ spacetimedb tool version 2.6.0; spacetimedb-lib version 2.6.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'));
|
||||
});
|
||||
@@ -858,10 +868,7 @@ spacetimedb tool version 2.6.0; spacetimedb-lib version 2.6.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(
|
||||
@@ -869,8 +876,8 @@ spacetimedb tool version 2.6.0; spacetimedb-lib version 2.6.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',
|
||||
});
|
||||
@@ -880,7 +887,7 @@ spacetimedb tool version 2.6.0; spacetimedb-lib version 2.6.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();
|
||||
@@ -899,6 +906,94 @@ spacetimedb tool version 2.6.0; spacetimedb-lib version 2.6.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;
|
||||
@@ -954,10 +1049,7 @@ spacetimedb tool version 2.6.0; spacetimedb-lib version 2.6.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(
|
||||
|
||||
Reference in New Issue
Block a user