修正 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
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:
@@ -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
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user