Merge branch 'master' into codex/puzzle-clear-template-runtime-fixes
# Conflicts: # .hermes/shared-memory/decision-log.md # .hermes/shared-memory/project-overview.md # docs/【开发运维】本地开发验证与生产运维-2026-05-15.md # scripts/dev.test.ts # server-rs/crates/api-server/src/creation_entry_config.rs # server-rs/crates/api-server/src/wooden_fish.rs # server-rs/crates/module-auth/src/lib.rs # server-rs/crates/spacetime-client/src/wooden_fish.rs # server-rs/crates/spacetime-module/src/auth/procedures.rs # src/components/custom-world-home/creationWorkShelf.ts # src/components/platform-entry/PlatformEntryFlowShellImpl.tsx # src/components/rpg-entry/rpgEntryWorldPresentation.ts # src/services/miniGameDraftGenerationProgress.test.ts # src/services/miniGameDraftGenerationProgress.ts
This commit is contained in:
@@ -400,6 +400,10 @@ if [[ "${BUILD_WEB}" -eq 1 ]]; then
|
||||
MAINTENANCE_HTML
|
||||
fi
|
||||
|
||||
echo "[production-release] 规范 Web 静态资源权限"
|
||||
find "${WEB_DIR}" -type d -exec chmod 755 {} +
|
||||
find "${WEB_DIR}" -type f -exec chmod 644 {} +
|
||||
|
||||
echo "[production-release] 打包 Web 静态资源 -> ${TARGET_DIR}/web.tar.gz"
|
||||
tar -czf "${TARGET_DIR}/web.tar.gz" -C "${TARGET_DIR}" web
|
||||
write_sha256_file "${TARGET_DIR}/web.tar.gz"
|
||||
|
||||
@@ -98,6 +98,10 @@ echo "[production-web-deploy] 解压 Web 到: ${WEB_TARGET}"
|
||||
tar -xzf "${SOURCE_DIR}/web.tar.gz" -C "${RELEASE_DIR}"
|
||||
test -d "${WEB_TARGET}"
|
||||
|
||||
echo "[production-web-deploy] 规范 Web 静态资源权限"
|
||||
find "${WEB_TARGET}" -type d -exec chmod 755 {} +
|
||||
find "${WEB_TARGET}" -type f -exec chmod 644 {} +
|
||||
|
||||
if [[ -f "${SOURCE_DIR}/release-manifest.json" ]]; then
|
||||
cp "${SOURCE_DIR}/release-manifest.json" "${RELEASE_DIR}/release-manifest.web.json"
|
||||
fi
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,13 @@
|
||||
import {createServer} from 'node:net';
|
||||
import {mkdtempSync, readFileSync, rmSync} from 'node:fs';
|
||||
import {tmpdir} from 'node:os';
|
||||
import {join} from 'node:path';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
import {
|
||||
findAvailablePort,
|
||||
mapDevPortsToPortRange,
|
||||
parsePortRangeSpec,
|
||||
reserveLinuxDevPortRange,
|
||||
resolveDevStackPorts,
|
||||
} from './dev-stack-port-utils.mjs';
|
||||
|
||||
@@ -18,6 +24,20 @@ function reservePort(port) {
|
||||
}
|
||||
|
||||
describe('dev stack port utils', () => {
|
||||
it('解析端口段并映射到四个 dev 端口', () => {
|
||||
expect(parsePortRangeSpec('10000-10099')).toEqual({
|
||||
start: 10000,
|
||||
end: 10099,
|
||||
label: '10000-10099',
|
||||
});
|
||||
expect(mapDevPortsToPortRange('10000-10099')).toMatchObject({
|
||||
webPort: 10000,
|
||||
apiPort: 10001,
|
||||
spacetimePort: 10002,
|
||||
adminWebPort: 10003,
|
||||
});
|
||||
});
|
||||
|
||||
it('使用端口可用性检查为被占用端口寻找后续可用端口', async () => {
|
||||
const firstServer = await reservePort(0);
|
||||
const firstPort = firstServer.address().port;
|
||||
@@ -38,6 +58,16 @@ describe('dev stack port utils', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('端口查找不会越过 Linux 用户端口段', async () => {
|
||||
await expect(
|
||||
findAvailablePort({
|
||||
host: '127.0.0.1',
|
||||
preferredPort: 9999,
|
||||
portRange: {start: 10000, end: 10099, label: '10000-10099'},
|
||||
}),
|
||||
).rejects.toThrow('不在允许端口段');
|
||||
});
|
||||
|
||||
it('为 npm run dev 的所有后续流程解析互不冲突的端口', async () => {
|
||||
const resolvedPorts = await resolveDevStackPorts({
|
||||
spacetime: {host: '127.0.0.1', preferredPort: 0},
|
||||
@@ -48,4 +78,191 @@ describe('dev stack port utils', () => {
|
||||
|
||||
expect(new Set(Object.values(resolvedPorts)).size).toBe(4);
|
||||
});
|
||||
|
||||
it('端口段内会一直漂移到段尾,不会被默认 200 次尝试截断', async () => {
|
||||
const rangeStart = 10000;
|
||||
const rangeEnd = 10300;
|
||||
const reservedPorts = new Set(
|
||||
Array.from({length: 201}, (_, index) => rangeStart + index),
|
||||
);
|
||||
|
||||
const availablePort = await findAvailablePort({
|
||||
host: '127.0.0.1',
|
||||
preferredPort: rangeStart,
|
||||
reservedPorts,
|
||||
portRange: {start: rangeStart, end: rangeEnd, label: `${rangeStart}-${rangeEnd}`},
|
||||
});
|
||||
|
||||
expect(availablePort).toBeGreaterThan(rangeStart + 200);
|
||||
expect(availablePort).toBeLessThanOrEqual(rangeEnd);
|
||||
});
|
||||
|
||||
const linuxIt = process.platform === 'linux' ? it : it.skip;
|
||||
|
||||
linuxIt('Linux 未手动指定端口段时从 10000 开始按 100 端口块自动分配', async () => {
|
||||
const tempRoot = mkdtempSync(join(tmpdir(), 'genarrative-port-range-'));
|
||||
const registryPath = join(tempRoot, 'registry.json');
|
||||
const lockPath = join(tempRoot, 'registry.lock');
|
||||
|
||||
try {
|
||||
const aliceAllocation = await reserveLinuxDevPortRange({
|
||||
env: {
|
||||
USER: 'alice',
|
||||
LOGNAME: 'alice',
|
||||
GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempRoot,
|
||||
},
|
||||
username: 'alice',
|
||||
requestedRange: null,
|
||||
registryPath,
|
||||
lockPath,
|
||||
});
|
||||
const bobAllocation = await reserveLinuxDevPortRange({
|
||||
env: {
|
||||
USER: 'bob',
|
||||
LOGNAME: 'bob',
|
||||
GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempRoot,
|
||||
},
|
||||
username: 'bob',
|
||||
requestedRange: null,
|
||||
registryPath,
|
||||
lockPath,
|
||||
});
|
||||
|
||||
expect(aliceAllocation.range.label).toBe('10000-10099');
|
||||
expect(aliceAllocation.source).toBe('auto');
|
||||
expect(bobAllocation.range.label).toBe('10100-10199');
|
||||
expect(bobAllocation.source).toBe('auto');
|
||||
} finally {
|
||||
rmSync(tempRoot, {recursive: true, force: true});
|
||||
}
|
||||
});
|
||||
|
||||
linuxIt('Linux 系统级端口段记录会阻止两个用户拿到同一段', async () => {
|
||||
const tempRoot = mkdtempSync(join(tmpdir(), 'genarrative-port-range-'));
|
||||
const registryPath = join(tempRoot, 'registry.json');
|
||||
const lockPath = join(tempRoot, 'registry.lock');
|
||||
|
||||
try {
|
||||
const baseEnv = {
|
||||
USER: 'alice',
|
||||
LOGNAME: 'alice',
|
||||
GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempRoot,
|
||||
GENARRATIVE_DEV_PORT_RANGE: '10000-10099',
|
||||
};
|
||||
|
||||
const aliceAllocation = await reserveLinuxDevPortRange({
|
||||
env: baseEnv,
|
||||
username: 'alice',
|
||||
registryPath,
|
||||
lockPath,
|
||||
});
|
||||
|
||||
expect(aliceAllocation.range.label).toBe('10000-10099');
|
||||
|
||||
const registryAfterAlice = JSON.parse(readFileSync(registryPath, 'utf8'));
|
||||
expect(registryAfterAlice.allocations.alice.range.label).toBe('10000-10099');
|
||||
|
||||
await expect(
|
||||
reserveLinuxDevPortRange({
|
||||
env: {
|
||||
...baseEnv,
|
||||
USER: 'bob',
|
||||
LOGNAME: 'bob',
|
||||
},
|
||||
username: 'bob',
|
||||
requestedRange: '10000-10099',
|
||||
registryPath,
|
||||
lockPath,
|
||||
}),
|
||||
).rejects.toThrow('已被用户 alice 占用');
|
||||
|
||||
const aliceReuse = await reserveLinuxDevPortRange({
|
||||
env: {
|
||||
...baseEnv,
|
||||
USER: 'alice',
|
||||
LOGNAME: 'alice',
|
||||
},
|
||||
username: 'alice',
|
||||
registryPath,
|
||||
lockPath,
|
||||
});
|
||||
|
||||
expect(aliceReuse.range.label).toBe('10000-10099');
|
||||
expect(aliceReuse.source).toBe('manual');
|
||||
expect(
|
||||
(
|
||||
await reserveLinuxDevPortRange({
|
||||
env: {
|
||||
...baseEnv,
|
||||
USER: 'alice',
|
||||
LOGNAME: 'alice',
|
||||
GENARRATIVE_DEV_PORT_RANGE: '10100-10199',
|
||||
},
|
||||
username: 'alice',
|
||||
requestedRange: '10100-10199',
|
||||
registryPath,
|
||||
lockPath,
|
||||
})
|
||||
).range.label,
|
||||
).toBe('10000-10099');
|
||||
|
||||
const registryAfterReuse = JSON.parse(readFileSync(registryPath, 'utf8'));
|
||||
expect(registryAfterReuse.allocations.alice.range.label).toBe('10000-10099');
|
||||
|
||||
const bobAllocation = await reserveLinuxDevPortRange({
|
||||
env: {
|
||||
...baseEnv,
|
||||
USER: 'bob',
|
||||
LOGNAME: 'bob',
|
||||
GENARRATIVE_DEV_PORT_RANGE: '10100-10199',
|
||||
},
|
||||
username: 'bob',
|
||||
requestedRange: '10100-10199',
|
||||
registryPath,
|
||||
lockPath,
|
||||
});
|
||||
|
||||
expect(bobAllocation.range.label).toBe('10100-10199');
|
||||
} finally {
|
||||
rmSync(tempRoot, {recursive: true, force: true});
|
||||
}
|
||||
});
|
||||
|
||||
linuxIt('Linux 同一用户第二个 dev 会话会复用同一用户段并继续在段内漂移', async () => {
|
||||
const tempRoot = mkdtempSync(join(tmpdir(), 'genarrative-port-range-'));
|
||||
const registryPath = join(tempRoot, 'registry.json');
|
||||
const lockPath = join(tempRoot, 'registry.lock');
|
||||
|
||||
try {
|
||||
const baseEnv = {
|
||||
USER: 'alice',
|
||||
LOGNAME: 'alice',
|
||||
GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempRoot,
|
||||
GENARRATIVE_DEV_PORT_RANGE: '10000-10099',
|
||||
};
|
||||
|
||||
const first = await reserveLinuxDevPortRange({
|
||||
env: baseEnv,
|
||||
username: 'alice',
|
||||
registryPath,
|
||||
lockPath,
|
||||
});
|
||||
|
||||
const second = await reserveLinuxDevPortRange({
|
||||
env: baseEnv,
|
||||
username: 'alice',
|
||||
registryPath,
|
||||
lockPath,
|
||||
});
|
||||
|
||||
expect(first.range.label).toBe('10000-10099');
|
||||
expect(second.range.label).toBe('10000-10099');
|
||||
expect(second.username).toBe('alice');
|
||||
expect(JSON.parse(readFileSync(registryPath, 'utf8')).allocations.alice.range.label).toBe(
|
||||
'10000-10099',
|
||||
);
|
||||
} finally {
|
||||
rmSync(tempRoot, {recursive: true, force: true});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+383
-9
File diff suppressed because it is too large
Load Diff
+142
-4
@@ -9,6 +9,7 @@ import {
|
||||
assertReusableSpacetimeProcessVersionMatchesWorkspace,
|
||||
assertSpacetimeToolVersionMatchesWorkspace,
|
||||
buildApiServerProcessEnv,
|
||||
buildDevStackSnapshot,
|
||||
buildSpacetimePublishArgs,
|
||||
createDevServerSpawnOptions,
|
||||
createWatchConfigs,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
isSpacetimePublishPermissionError,
|
||||
parseSpacetimeToolVersion,
|
||||
parseArgs,
|
||||
resolveDevStackStatePath,
|
||||
shouldAcceptWatchEvent,
|
||||
} from './dev.mjs';
|
||||
|
||||
@@ -36,6 +38,8 @@ function workspaceSpacetimeVersionForTest() {
|
||||
}
|
||||
|
||||
describe('dev scheduler argument routing', () => {
|
||||
const linuxTest = process.platform === 'linux' ? test : test.skip;
|
||||
|
||||
test('Windows junction 路径下的直接执行入口也能识别为当前模块', () => {
|
||||
const moduleUrl =
|
||||
'file:///F:/DevWorktrees/codex/worktrees/f584/Genarrative/scripts/dev.mjs';
|
||||
@@ -102,6 +106,67 @@ describe('dev scheduler argument routing', () => {
|
||||
'http://127.0.0.1:3100',
|
||||
);
|
||||
});
|
||||
|
||||
linuxTest('Linux 启动时按系统级端口段映射四个 dev 端口', async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-dev-port-range-'));
|
||||
try {
|
||||
const {command, explicitOptions, options} = parseArgs([], {
|
||||
USER: 'alice',
|
||||
LOGNAME: 'alice',
|
||||
GENARRATIVE_DEV_PORT_RANGE: '22000-22099',
|
||||
GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempDir,
|
||||
});
|
||||
const runner = new DevRunner(options, {
|
||||
USER: 'alice',
|
||||
LOGNAME: 'alice',
|
||||
GENARRATIVE_DEV_PORT_RANGE: '22000-22099',
|
||||
GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempDir,
|
||||
}, explicitOptions);
|
||||
|
||||
await runner.prepareLinuxPortRange(command);
|
||||
|
||||
expect(runner.state.portRange.label).toBe('22000-22099');
|
||||
expect(runner.options.webPort).toBe(22000);
|
||||
expect(runner.options.apiPort).toBe(22001);
|
||||
expect(runner.options.spacetimePort).toBe(22002);
|
||||
expect(runner.options.adminWebPort).toBe(22003);
|
||||
expect(runner.state.apiTarget).toBe('http://127.0.0.1:22001');
|
||||
expect(runner.state.spacetimeServer).toBe('http://127.0.0.1:22002');
|
||||
} finally {
|
||||
rmSync(tempDir, {recursive: true, force: true});
|
||||
}
|
||||
});
|
||||
|
||||
test('Windows 仍沿用原有端口解析,不启用 Linux 端口段登记', async () => {
|
||||
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
|
||||
Object.defineProperty(process, 'platform', {
|
||||
configurable: true,
|
||||
value: 'win32',
|
||||
});
|
||||
|
||||
try {
|
||||
const {command, explicitOptions, options} = parseArgs([], {
|
||||
USER: 'alice',
|
||||
GENARRATIVE_DEV_PORT_RANGE: '22000-22099',
|
||||
});
|
||||
const runner = new DevRunner(options, {
|
||||
USER: 'alice',
|
||||
GENARRATIVE_DEV_PORT_RANGE: '22000-22099',
|
||||
}, explicitOptions);
|
||||
|
||||
await runner.prepareLinuxPortRange(command);
|
||||
|
||||
expect(runner.state.portRange).toBeNull();
|
||||
expect(runner.options.webPort).toBe(3000);
|
||||
expect(runner.options.apiPort).toBe(8082);
|
||||
expect(runner.options.spacetimePort).toBe(3101);
|
||||
expect(runner.options.adminWebPort).toBe(3102);
|
||||
} finally {
|
||||
if (originalPlatform) {
|
||||
Object.defineProperty(process, 'platform', originalPlatform);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('dev scheduler api-server env', () => {
|
||||
@@ -119,6 +184,79 @@ describe('dev scheduler api-server env', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('dev scheduler stack state file', () => {
|
||||
test('状态文件路径固定在根目录 .app/dev-stack.json', () => {
|
||||
expect(resolveDevStackStatePath('C:\\repo\\Genarrative')).toBe(
|
||||
join('C:\\repo\\Genarrative', '.app/dev-stack.json'),
|
||||
);
|
||||
});
|
||||
|
||||
test('状态快照记录服务 pid、端口、URL 和当前命令', () => {
|
||||
const updatedAt = '2026-05-29T00:00:00.000Z';
|
||||
const runner = {
|
||||
command: 'web',
|
||||
options: {
|
||||
apiHost: '127.0.0.1',
|
||||
apiPort: 8090,
|
||||
webHost: '0.0.0.0',
|
||||
webPort: 3010,
|
||||
adminWebHost: '127.0.0.1',
|
||||
adminWebPort: 3110,
|
||||
spacetimeHost: '127.0.0.1',
|
||||
spacetimePort: 3120,
|
||||
spacetimeDataDir: 'server-rs/.spacetimedb/local/data',
|
||||
database: 'genarrative-test',
|
||||
watch: false,
|
||||
},
|
||||
state: {
|
||||
apiTarget: 'http://127.0.0.1:8090',
|
||||
adminWebTargetHost: '127.0.0.1',
|
||||
spacetimeServer: 'http://127.0.0.1:3120',
|
||||
},
|
||||
services: new Map([
|
||||
[
|
||||
'web',
|
||||
{
|
||||
child: {pid: 4321},
|
||||
runtime: {
|
||||
status: 'running',
|
||||
pid: 4321,
|
||||
host: '0.0.0.0',
|
||||
port: 3010,
|
||||
url: 'http://127.0.0.1:3010',
|
||||
command: 'node scripts/vite-cli.mjs --port=3010',
|
||||
startedAt: updatedAt,
|
||||
updatedAt,
|
||||
exitCode: null,
|
||||
signal: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
]),
|
||||
};
|
||||
|
||||
const snapshot = buildDevStackSnapshot(runner, updatedAt);
|
||||
|
||||
expect(snapshot.schemaVersion).toBe(1);
|
||||
expect(snapshot.command).toBe('web');
|
||||
expect(snapshot.database).toBe('genarrative-test');
|
||||
expect(snapshot.services.web).toMatchObject({
|
||||
status: 'running',
|
||||
pid: 4321,
|
||||
host: '0.0.0.0',
|
||||
port: 3010,
|
||||
url: 'http://127.0.0.1:3010',
|
||||
command: 'node scripts/vite-cli.mjs --port=3010',
|
||||
});
|
||||
expect(snapshot.services['api-server']).toMatchObject({
|
||||
status: 'idle',
|
||||
pid: null,
|
||||
port: 8090,
|
||||
url: 'http://127.0.0.1:8090',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dev scheduler spacetime reuse guard', () => {
|
||||
test('记录 URL 可 ping 但没有 spacetime.pid 时不复用宿主', async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-spacetime-reuse-'));
|
||||
@@ -266,18 +404,18 @@ describe('dev scheduler watch routing', () => {
|
||||
describe('dev scheduler spacetime refresh', () => {
|
||||
test('解析 spacetime --version 输出里的 tool version', () => {
|
||||
const version = parseSpacetimeToolVersion(`
|
||||
A new version of SpacetimeDB is available: v2.2.0 (current: v2.1.0)
|
||||
spacetimedb tool version 2.2.0; spacetimedb-lib version 2.2.0;
|
||||
A new version of SpacetimeDB is available: v2.3.0 (current: v2.2.0)
|
||||
spacetimedb tool version 2.3.0; spacetimedb-lib version 2.3.0;
|
||||
`);
|
||||
|
||||
expect(version).toBe('2.2.0');
|
||||
expect(version).toBe('2.3.0');
|
||||
});
|
||||
|
||||
test('本机 spacetime 版本和 workspace 锁定版本不一致时直接报清楚', () => {
|
||||
expect(() =>
|
||||
assertSpacetimeToolVersionMatchesWorkspace({
|
||||
toolVersion: '2.1.0',
|
||||
workspaceVersion: '2.2.0',
|
||||
workspaceVersion: '2.3.0',
|
||||
}),
|
||||
).toThrow('procedure 返回值 BSATN 反序列化失败');
|
||||
});
|
||||
|
||||
Regular → Executable
+36
-1
@@ -8,6 +8,16 @@ set -euo pipefail
|
||||
|
||||
ORIGINAL_HOME="${HOME:-}"
|
||||
CARGO_BUILD_HOME="${CARGO_BUILD_HOME:-$(dirname "${CARGO_HOME}")/home}"
|
||||
USER_CARGO_CONFIG=""
|
||||
WORKSPACE_TMP_ROOT="${WORKSPACE_TMP:-}"
|
||||
if [[ -z "${WORKSPACE_TMP_ROOT}" && -n "${WORKSPACE:-}" ]]; then
|
||||
WORKSPACE_TMP_ROOT="${WORKSPACE}@tmp"
|
||||
fi
|
||||
if [[ -z "${WORKSPACE_TMP_ROOT}" ]]; then
|
||||
WORKSPACE_TMP_ROOT="${CARGO_BUILD_HOME}"
|
||||
fi
|
||||
SCCACHE_SERVER_UDS="${SCCACHE_SERVER_UDS:-${WORKSPACE_TMP_ROOT}/sccache.sock}"
|
||||
SCCACHE_IDLE_TIMEOUT="${SCCACHE_IDLE_TIMEOUT:-0}"
|
||||
|
||||
mkdir -p "${CARGO_HOME}" "${CARGO_TARGET_DIR}" "${SCCACHE_DIR}" "${CARGO_BUILD_HOME}"
|
||||
|
||||
@@ -25,20 +35,45 @@ for tool_dir in "${ORIGINAL_HOME}/.cargo/bin" /root/.cargo/bin /usr/local/cargo/
|
||||
fi
|
||||
done
|
||||
|
||||
for candidate in \
|
||||
"${ORIGINAL_HOME}/.cargo/config.toml" \
|
||||
"${ORIGINAL_HOME}/.cargo/config" \
|
||||
"/data/jenkins/.cargo/config.toml" \
|
||||
"/data/jenkins/.cargo/config" \
|
||||
"/etc/cargo/config.toml" \
|
||||
"/etc/cargo/config"
|
||||
do
|
||||
if [[ -f "${candidate}" ]]; then
|
||||
USER_CARGO_CONFIG="${candidate}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
export HOME="${CARGO_BUILD_HOME}"
|
||||
export CARGO_HOME
|
||||
export CARGO_TARGET_DIR
|
||||
export SCCACHE_DIR
|
||||
export SCCACHE_SERVER_UDS
|
||||
export SCCACHE_IDLE_TIMEOUT
|
||||
|
||||
cat >"${CARGO_HOME}/config.toml" <<'CARGO_CONFIG'
|
||||
if [[ -n "${USER_CARGO_CONFIG}" ]]; then
|
||||
install -m 0644 "${USER_CARGO_CONFIG}" "${CARGO_HOME}/config.toml"
|
||||
else
|
||||
cat >"${CARGO_HOME}/config.toml" <<'CARGO_CONFIG'
|
||||
[registries.crates-io]
|
||||
protocol = "sparse"
|
||||
CARGO_CONFIG
|
||||
fi
|
||||
|
||||
echo "[cargo-env] HOME=${HOME}"
|
||||
echo "[cargo-env] CARGO_HOME=${CARGO_HOME}"
|
||||
echo "[cargo-env] CARGO_TARGET_DIR=${CARGO_TARGET_DIR}"
|
||||
echo "[cargo-env] SCCACHE_DIR=${SCCACHE_DIR}"
|
||||
echo "[cargo-env] SCCACHE_SERVER_UDS=${SCCACHE_SERVER_UDS}"
|
||||
echo "[cargo-env] SCCACHE_IDLE_TIMEOUT=${SCCACHE_IDLE_TIMEOUT}"
|
||||
if [[ -n "${USER_CARGO_CONFIG}" ]]; then
|
||||
echo "[cargo-env] USER_CARGO_CONFIG=${USER_CARGO_CONFIG}"
|
||||
fi
|
||||
if [[ -n "${RUSTUP_HOME:-}" ]]; then
|
||||
echo "[cargo-env] RUSTUP_HOME=${RUSTUP_HOME}"
|
||||
fi
|
||||
|
||||
@@ -17,6 +17,8 @@ type MiniProgramPage = {
|
||||
data: Record<string, unknown>;
|
||||
setData: (patch: Record<string, unknown>) => void;
|
||||
onLoad: (query?: Record<string, string>) => Promise<void>;
|
||||
onShow: () => void;
|
||||
consumePayResult: () => void;
|
||||
};
|
||||
|
||||
function createWxMock() {
|
||||
@@ -44,6 +46,10 @@ function loadWebViewPage(
|
||||
Page(config: Record<string, unknown>) {
|
||||
pageConfig = config;
|
||||
},
|
||||
setTimeout(callback: () => void) {
|
||||
callback();
|
||||
return 1;
|
||||
},
|
||||
require(requestPath: string) {
|
||||
if (requestPath === '../../config') {
|
||||
return {
|
||||
@@ -85,22 +91,40 @@ describe('mini-program web-view auth page', () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('默认进入时直接打开 web-view,不触发微信登录请求', async () => {
|
||||
test('默认进入时刷新微信小程序登录态后打开 web-view', async () => {
|
||||
const wxMock = createWxMock();
|
||||
wxMock.login.mockImplementation(({ success }) => {
|
||||
success({ code: 'wx-login-code' });
|
||||
});
|
||||
wxMock.request.mockImplementation(({ success }) => {
|
||||
success({
|
||||
statusCode: 200,
|
||||
data: {
|
||||
token: 'jwt-active-wechat',
|
||||
bindingStatus: 'active',
|
||||
},
|
||||
});
|
||||
});
|
||||
const page = loadWebViewPage(wxMock);
|
||||
|
||||
await page.onLoad({});
|
||||
|
||||
expect(wxMock.login).not.toHaveBeenCalled();
|
||||
expect(wxMock.request).not.toHaveBeenCalled();
|
||||
expect(wxMock.login).toHaveBeenCalledTimes(1);
|
||||
expect(wxMock.request).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: 'https://www.genarrative.world/api/auth/wechat/miniprogram-login',
|
||||
method: 'POST',
|
||||
data: { code: 'wx-login-code' },
|
||||
}),
|
||||
);
|
||||
expect(page.data.loading).toBe(false);
|
||||
expect(page.data.phoneBindingRequired).toBe(false);
|
||||
expect(page.data.webViewUrl).toBe(
|
||||
'https://www.genarrative.world/?clientType=mini_program&clientRuntime=wechat_mini_program',
|
||||
'https://www.genarrative.world/?clientType=mini_program&clientRuntime=wechat_mini_program#auth_provider=wechat&auth_token=jwt-active-wechat&auth_binding_status=active',
|
||||
);
|
||||
});
|
||||
|
||||
test('默认匿名进入 web-view 不依赖 API_BASE_URL 配置', async () => {
|
||||
test('默认匿名进入 web-view 仍不依赖 API_BASE_URL 配置', async () => {
|
||||
const wxMock = createWxMock();
|
||||
const page = loadWebViewPage(wxMock, {
|
||||
API_BASE_URL: '',
|
||||
@@ -116,6 +140,27 @@ describe('mini-program web-view auth page', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('onShow 二次检查支付结果并写回 web-view hash', () => {
|
||||
const wxMock = createWxMock();
|
||||
wxMock.getStorageSync.mockImplementation((key) =>
|
||||
key === 'genarrative:wechat-pay-result'
|
||||
? 'request-1:success:order-1'
|
||||
: '',
|
||||
);
|
||||
const page = loadWebViewPage(wxMock);
|
||||
page.data.webViewUrl =
|
||||
'https://www.genarrative.world/?clientType=mini_program#tab=profile';
|
||||
|
||||
page.onShow();
|
||||
|
||||
expect(wxMock.removeStorageSync).toHaveBeenCalledWith(
|
||||
'genarrative:wechat-pay-result',
|
||||
);
|
||||
expect(page.data.webViewUrl).toBe(
|
||||
'https://www.genarrative.world/?clientType=mini_program#tab=profile&wx_pay_result=request-1%3Asuccess%3Aorder-1',
|
||||
);
|
||||
});
|
||||
|
||||
test('H5 请求登录时才启动微信小程序登录并进入手机号授权态', async () => {
|
||||
const wxMock = createWxMock();
|
||||
wxMock.login.mockImplementation(({ success }) => {
|
||||
|
||||
Reference in New Issue
Block a user