修复AI游戏创作启动评审问题
将本地发布身份改为按SpacetimeDB数据目录持久化并兼容迁移旧记录 为开发栈状态补充专用数据目录并拒绝复用旧共享后端 统一处理子进程启动错误、启动期信号和进程组清理 补充身份、后端复用与子进程生命周期回归测试及文档
This commit is contained in:
+101
-24
@@ -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,
|
||||
@@ -827,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'));
|
||||
});
|
||||
@@ -873,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(
|
||||
@@ -884,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',
|
||||
});
|
||||
@@ -895,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();
|
||||
@@ -914,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;
|
||||
@@ -969,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