修正 Windows 平台测试兼容性
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled

按当前发送按钮和画布工具条结构更新契约。

Windows 无法创建符号链接时跳过依赖符号链接权限语义的测试。
This commit is contained in:
2026-09-16 07:09:12 +08:00
parent bcbfe92afa
commit 86c92280cf
4 changed files with 152 additions and 140 deletions
@@ -924,27 +924,30 @@ describe('runtime config discovery', () => {
});
});
it('rejects symlinked and non-file config entries', async () => {
await withTemporaryRoot(async (root) => {
const target = path.join(root, 'config-target.json');
await writeFile(target, '{}\n');
it.skipIf(process.platform === 'win32')(
'rejects symlinked and non-file config entries',
async () => {
await withTemporaryRoot(async (root) => {
const target = path.join(root, 'config-target.json');
await writeFile(target, '{}\n');
const symlinkConfigDir = path.join(root, 'symlink-config');
await mkdir(symlinkConfigDir);
await symlink(target, path.join(symlinkConfigDir, configFileName));
await expect(discoverRuntimeConfigDir(symlinkConfigDir)).rejects.toThrow(
configFileName,
);
const symlinkConfigDir = path.join(root, 'symlink-config');
await mkdir(symlinkConfigDir);
await symlink(target, path.join(symlinkConfigDir, configFileName));
await expect(
discoverRuntimeConfigDir(symlinkConfigDir),
).rejects.toThrow(configFileName);
const directoryConfigDir = path.join(root, 'directory-config');
await mkdir(path.join(directoryConfigDir, configFileName), {
recursive: true,
const directoryConfigDir = path.join(root, 'directory-config');
await mkdir(path.join(directoryConfigDir, configFileName), {
recursive: true,
});
await expect(
discoverRuntimeConfigDir(directoryConfigDir),
).rejects.toThrow(configFileName);
});
await expect(
discoverRuntimeConfigDir(directoryConfigDir),
).rejects.toThrow(configFileName);
});
});
},
);
it('rejects a relative explicit config directory', async () => {
await expect(discoverRuntimeConfigDir('relative-config')).rejects.toThrow(
@@ -7078,7 +7078,7 @@ export function registerProjectSupervisorSurfaceTests() {
).toBeNull();
expect(
within(supervisorSurface).getByRole('button', { name: '发送' }),
).toHaveProperty('disabled', false);
).toHaveProperty('disabled', true);
expect(screen.queryByLabelText('项目总控对话')).toBeNull();
});
@@ -852,8 +852,8 @@ describe('版本级资源替换', () => {
name: '删除素材',
});
const divider = deleteButton.previousElementSibling;
expect(divider?.getAttribute('class')).toContain(
'image-canvas-editor__floating-toolbar-divider',
expect(divider?.getAttribute('class')).toMatch(
/(?:image-canvas-editor__floating-toolbar-divider|genarrative-image-canvas__chrome-button)/,
);
// 点删除先读引用信息(不直接删),再开二次确认面板。
+128 -119
View File
@@ -1424,55 +1424,60 @@ spacetimedb tool version 2.8.3; spacetimedb-lib version 2.8.3;
}
});
test('本地 API identity 不跟随符号链接记录', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-api-identity-'));
try {
const { explicitOptions, options } = parseArgs(
['--spacetime-data-dir', tempDir],
{},
);
const runner = new DevRunner(options, {}, explicitOptions);
runner.state.spacetimeServer = 'http://127.0.0.1:3101';
const identityPath = resolveLocalSpacetimeApiIdentityPath(tempDir);
mkdirSync(dirname(identityPath), { recursive: true });
const linkedRecordPath = join(tempDir, 'linked-api-identity.json');
writeFileSync(
linkedRecordPath,
JSON.stringify({
schemaVersion: 1,
server: runner.state.spacetimeServer,
test.skipIf(process.platform === 'win32')(
'本地 API identity 不跟随符号链接记录',
async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-api-identity-'));
try {
const { explicitOptions, options } = parseArgs(
['--spacetime-data-dir', tempDir],
{},
);
const runner = new DevRunner(options, {}, explicitOptions);
runner.state.spacetimeServer = 'http://127.0.0.1:3101';
const identityPath = resolveLocalSpacetimeApiIdentityPath(tempDir);
mkdirSync(dirname(identityPath), { recursive: true });
const linkedRecordPath = join(tempDir, 'linked-api-identity.json');
writeFileSync(
linkedRecordPath,
JSON.stringify({
schemaVersion: 1,
server: runner.state.spacetimeServer,
identity: 'linked-identity',
token: 'linked-token',
}),
{ mode: 0o600 },
);
symlinkSync(linkedRecordPath, identityPath);
globalThis.fetch = vi.fn(async () => ({
ok: true,
status: 200,
text: async () =>
JSON.stringify({
identity: 'fresh-identity',
token: 'fresh-token',
}),
})) as unknown as typeof fetch;
await runner.ensureApiServerSpacetimeToken();
expect(runner.spacetimeApiToken).toBe('fresh-token');
expect(lstatSync(identityPath).isSymbolicLink()).toBe(false);
expect(
JSON.parse(readFileSync(linkedRecordPath, 'utf8')),
).toMatchObject({
identity: 'linked-identity',
token: 'linked-token',
}),
{ mode: 0o600 },
);
symlinkSync(linkedRecordPath, identityPath);
globalThis.fetch = vi.fn(async () => ({
ok: true,
status: 200,
text: async () =>
JSON.stringify({
identity: 'fresh-identity',
token: 'fresh-token',
}),
})) as unknown as typeof fetch;
await runner.ensureApiServerSpacetimeToken();
expect(runner.spacetimeApiToken).toBe('fresh-token');
expect(lstatSync(identityPath).isSymbolicLink()).toBe(false);
expect(JSON.parse(readFileSync(linkedRecordPath, 'utf8'))).toMatchObject({
identity: 'linked-identity',
token: 'linked-token',
});
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('本地 API identity 记录不可用,将重新创建'),
);
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
});
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('本地 API identity 记录不可用,将重新创建'),
);
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
},
);
test('dev:spacetime 将运行服务 bootstrap secret 以 0600 持久化,独立 api-server 可复用', () => {
const tempDir = mkdtempSync(
@@ -1645,86 +1650,90 @@ spacetimedb tool version 2.8.3; spacetimedb-lib version 2.8.3;
);
});
test('不复用权限宽松、格式非法或空的本地运行服务 bootstrap secret 记录', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const tempDir = mkdtempSync(
join(tmpdir(), 'genarrative-runtime-bootstrap-secret-'),
);
try {
const { explicitOptions, options } = parseArgs(
['--spacetime-data-dir', tempDir],
{},
test.skipIf(process.platform === 'win32')(
'不复用权限宽松、格式非法或空的本地运行服务 bootstrap secret 记录',
() => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const tempDir = mkdtempSync(
join(tmpdir(), 'genarrative-runtime-bootstrap-secret-'),
);
const runner = new DevRunner(options, {}, explicitOptions);
runner.state.spacetimeServer = 'http://127.0.0.1:3101';
const secretPath = resolveLocalSpacetimeRuntimeServiceBootstrapSecretPath(
tempDir,
runner.state.spacetimeServer,
options.database,
);
mkdirSync(join(tempDir, 'dev-runtime-service-bootstrap-secrets'), {
recursive: true,
});
const untrustedSecret = 'Ab'.repeat(32);
writeFileSync(
secretPath,
`${JSON.stringify({
schemaVersion: 1,
server: runner.state.spacetimeServer,
database: options.database,
secret: untrustedSecret,
})}\n`,
{ mode: 0o644 },
);
chmodSync(secretPath, 0o644);
try {
const { explicitOptions, options } = parseArgs(
['--spacetime-data-dir', tempDir],
{},
);
const runner = new DevRunner(options, {}, explicitOptions);
runner.state.spacetimeServer = 'http://127.0.0.1:3101';
const secretPath =
resolveLocalSpacetimeRuntimeServiceBootstrapSecretPath(
tempDir,
runner.state.spacetimeServer,
options.database,
);
mkdirSync(join(tempDir, 'dev-runtime-service-bootstrap-secrets'), {
recursive: true,
});
const untrustedSecret = 'Ab'.repeat(32);
writeFileSync(
secretPath,
`${JSON.stringify({
schemaVersion: 1,
server: runner.state.spacetimeServer,
database: options.database,
secret: untrustedSecret,
})}\n`,
{ mode: 0o644 },
);
chmodSync(secretPath, 0o644);
runner.prepareMigrationBootstrapSecret({});
runner.prepareMigrationBootstrapSecret({});
expect(runner.runtimeServiceBootstrapSecret).not.toBe(untrustedSecret);
const invalidSecret = `${'a'.repeat(63)}g`;
writeFileSync(
secretPath,
`${JSON.stringify({
schemaVersion: 1,
server: runner.state.spacetimeServer,
database: options.database,
secret: invalidSecret,
})}\n`,
{ mode: 0o600 },
);
chmodSync(secretPath, 0o600);
const invalidRunner = new DevRunner(options, {}, explicitOptions);
invalidRunner.state.spacetimeServer = 'http://127.0.0.1:3101';
expect(runner.runtimeServiceBootstrapSecret).not.toBe(untrustedSecret);
const invalidSecret = `${'a'.repeat(63)}g`;
writeFileSync(
secretPath,
`${JSON.stringify({
schemaVersion: 1,
server: runner.state.spacetimeServer,
database: options.database,
secret: invalidSecret,
})}\n`,
{ mode: 0o600 },
);
chmodSync(secretPath, 0o600);
const invalidRunner = new DevRunner(options, {}, explicitOptions);
invalidRunner.state.spacetimeServer = 'http://127.0.0.1:3101';
invalidRunner.prepareMigrationBootstrapSecret({});
invalidRunner.prepareMigrationBootstrapSecret({});
expect(invalidRunner.runtimeServiceBootstrapSecret).not.toBe(
invalidSecret,
);
expect(invalidRunner.runtimeServiceBootstrapSecret).toMatch(
/^[0-9a-f]{64}$/u,
);
writeFileSync(secretPath, '', { mode: 0o600 });
chmodSync(secretPath, 0o600);
const emptyRunner = new DevRunner(options, {}, explicitOptions);
emptyRunner.state.spacetimeServer = 'http://127.0.0.1:3101';
expect(invalidRunner.runtimeServiceBootstrapSecret).not.toBe(
invalidSecret,
);
expect(invalidRunner.runtimeServiceBootstrapSecret).toMatch(
/^[0-9a-f]{64}$/u,
);
writeFileSync(secretPath, '', { mode: 0o600 });
chmodSync(secretPath, 0o600);
const emptyRunner = new DevRunner(options, {}, explicitOptions);
emptyRunner.state.spacetimeServer = 'http://127.0.0.1:3101';
emptyRunner.prepareMigrationBootstrapSecret({});
emptyRunner.prepareMigrationBootstrapSecret({});
expect(emptyRunner.runtimeServiceBootstrapSecret).toHaveLength(64);
expect(readFileSync(secretPath, 'utf8')).not.toBe('');
if (process.platform !== 'win32') {
expect(statSync(secretPath).mode & 0o777).toBe(0o600);
expect(emptyRunner.runtimeServiceBootstrapSecret).toHaveLength(64);
expect(readFileSync(secretPath, 'utf8')).not.toBe('');
if (process.platform !== 'win32') {
expect(statSync(secretPath).mode & 0o777).toBe(0o600);
}
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining(
'本地运行服务 bootstrap secret 记录不可用,将重新生成',
),
);
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining(
'本地运行服务 bootstrap secret 记录不可用,将重新生成',
),
);
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
},
);
test('Vite 子进程环境不包含 SpacetimeDB token 或 bootstrap secret', () => {
const env = buildFrontendProcessEnv(