Merge remote-tracking branch 'origin/master' into feat/pricing-plan
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m28s
Project CI / Backend tests (pull_request) Failing after 53s
Project CI / Native shell tests (pull_request) Failing after 1m33s
Project CI / Frontend tests (pull_request) Failing after 1m2s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m43s
Project CI / AI game creator shell web tests (pull_request) Failing after 45s
Project CI / Repository checks (pull_request) Failing after 1m3s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m24s

This commit is contained in:
2026-10-03 13:36:04 +08:00
438 changed files with 18965 additions and 242444 deletions
-6
View File
@@ -120,12 +120,6 @@ echo '[rust-cache] warming AGC Rust lanes'
cargo test --locked --manifest-path Cargo.toml \
--bin genarrative-ai-game-creator-shell --no-run
)
cargo clean --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml
echo '[rust-cache] warming AGC agent-run smoke executable'
(
cd apps/ai-game-creator-shell
cargo build --manifest-path src-tauri/Cargo.toml
)
du -sh /opt/genarrative-ci/rust-cache/objects
WARM
docker cp "${container_id}:/opt/genarrative-ci/rust-cache/." "${work_dir}/snapshot/"
-11
View File
@@ -78,8 +78,6 @@ const aiGameCreatorViteConfigSource = fs.readFileSync(
// - agc-rust-shard-1..4:AGC 壳自身的 Rust bin target 单测,按名单切成 4 片,片内仍保持
// `--test-threads=1`;CI 由两条 lane job 各顺序运行两片,每个分片分组都会做一次
// 「片并集等于全集且互斥」的自校验。
// - agc-rust-smoke:会用 `src-tauri/Cargo.toml` spawn `cargo run` 的 agent-run smoke。
// 与分片分开,免得把已经压到 4 分钟级的片 job 拖长。
// - release:发布构建 smoke 和依赖发布产物的落盘断言。
const nativeShellGateGroups = [
'contract',
@@ -90,7 +88,6 @@ const nativeShellGateGroups = [
'agc-rust-shard-2',
'agc-rust-shard-3',
'agc-rust-shard-4',
'agc-rust-smoke',
'release',
];
const requestedNativeShellGroups = readRequestedNativeShellGroups(
@@ -2396,14 +2393,6 @@ const steps = [
'--shard-index=4',
],
},
// agent-run smoke 会用 `src-tauri/Cargo.toml` spawn `cargo run`,因此单独一个分组,
// 落在同样预热了 AGC 壳 Cargo 依赖的 job 里。
{
group: 'agc-rust-smoke',
label: 'ai-game-creator-shell-agent-run-smoke',
command: npmCommand,
args: ['run', 'ai-game-creator-shell:agent-run:smoke'],
},
{
group: 'release',
label: 'ai-game-creator-shell-release-build-smoke',
+624
View File
File diff suppressed because it is too large Load Diff
+252
View File
@@ -0,0 +1,252 @@
import { EventEmitter } from 'node:events';
import { createServer } from 'node:http';
import type { AddressInfo } from 'node:net';
import { resolve } from 'node:path';
import { describe, expect, test } from 'vitest';
import {
agcDatabase,
agcDataDir,
buildDevAllBaseEnvironment,
buildDevAllEnvironment,
buildManagedSpawnOptions,
buildRootDevArgs,
buildStartupSummary,
isHttpReady,
isSharedRootStackReady,
renderStartupTable,
runDevAll,
validateDevAllArgs,
} from './dev-all.mjs';
type FakeChild = EventEmitter & {
pid: number;
exitCode: number | null;
signalCode: string | null;
};
function makeReadyState(root = resolve('fixture-root')) {
const instanceId = 'dev-fixture';
const service = (status = 'running', url = 'http://127.0.0.1:1') => ({
repoRoot: root,
instanceId,
status,
url,
});
return {
command: 'all',
instanceId,
repoRoot: root,
database: agcDatabase,
spacetimeDataDir: agcDataDir,
services: {
spacetime: service('reused', 'http://127.0.0.1:3101'),
'api-server': service('running', 'http://127.0.0.1:8082'),
'bgfilter-worker': service('running', 'http://127.0.0.1:8083'),
web: service('running', 'http://127.0.0.1:3000'),
'admin-web': service('running', 'http://127.0.0.1:3102/admin/'),
},
};
}
describe('dev:all 参数和共享开发栈合同', () => {
test('根 dev 固定使用 AGC 配套数据库,并保留调用方选项', () => {
expect(buildRootDevArgs(['--watch', '--api-port', '9082'])).toEqual([
'run',
'dev',
'--',
'--database',
agcDatabase,
'--spacetime-data-dir',
'server-rs/.spacetimedb/ai-game-creator/data',
'--preserve-database',
'--no-interactive',
'--watch',
'--api-port',
'9082',
]);
});
test('拒绝会破坏单后端拓扑的数据库和 data dir 覆盖', () => {
expect(() => validateDevAllArgs(['--database', 'other'])).toThrow(
'不能覆盖 --database 或 --spacetime-data-dir',
);
expect(() => validateDevAllArgs(['--database=other'])).toThrow(
'不能覆盖 --database 或 --spacetime-data-dir',
);
expect(() => validateDevAllArgs(['--spacetime-data-dir', 'other'])).toThrow(
'不能覆盖 --database 或 --spacetime-data-dir',
);
expect(() => validateDevAllArgs(['backend'])).toThrow('只接受 dev 选项');
});
test('预留 AGC Vite 端口并关闭 AGC 自带后台', () => {
const baseEnv = { AGC_DEV_ADMIN_WEB: '1' };
const env = buildDevAllEnvironment(baseEnv, 3080);
expect(env.GENARRATIVE_AGC_VITE_PORT).toBe('3080');
expect(env.AGC_DEV_ADMIN_WEB).toBe('0');
expect(baseEnv.AGC_DEV_ADMIN_WEB).toBe('1');
});
test('CLI port-range 覆盖旧环境并同步给 AGC', () => {
const env = buildDevAllBaseEnvironment(['--port-range', '10100-10199'], {
GENARRATIVE_DEV_PORT_RANGE: '10000-10099',
});
expect(env.GENARRATIVE_DEV_PORT_RANGE).toBe('10100-10199');
});
test.each([
[200, true],
[204, true],
[299, true],
[400, false],
[401, false],
[403, false],
[404, false],
[500, false],
[503, false],
])('HTTP %i 的前端就绪结果为 %s', async (status, expected) => {
const server = createServer((_request, response) => {
response.writeHead(status).end();
});
await new Promise<void>((resolveListen) => {
server.listen(0, '127.0.0.1', resolveListen);
});
try {
const { port } = server.address() as AddressInfo;
await expect(isHttpReady(`http://127.0.0.1:${port}`)).resolves.toBe(
expected,
);
} finally {
server.closeAllConnections();
await new Promise<void>((resolveClose, rejectClose) => {
server.close((error) => {
if (error) rejectClose(error);
else resolveClose();
});
});
}
});
test('Windows 下通过 shell 启动 npm.cmd', () => {
const options = buildManagedSpawnOptions(process.env);
expect(options.shell).toBe(process.platform === 'win32');
expect(options.windowsHide).toBe(process.platform === 'win32');
expect(options.detached).toBe(process.platform !== 'win32');
});
test('按最终端口构造完整启动摘要表', () => {
const rows = buildStartupSummary(makeReadyState(), {
url: 'http://127.0.0.1:3080/',
});
expect(rows).toHaveLength(7);
expect(rows).toEqual(
expect.arrayContaining([
expect.objectContaining({
项目: 'SpacetimeDB',
状态: '已复用',
地址: 'http://127.0.0.1:3101',
}),
expect.objectContaining({
项目: 'Admin 后台',
地址: 'http://127.0.0.1:3102/admin/',
}),
expect.objectContaining({
项目: 'AGC Vite',
地址: 'http://127.0.0.1:3080/',
}),
]),
);
});
test('渲染摘要表时不输出字符串引号', () => {
const rows = buildStartupSummary(makeReadyState(), {
url: 'http://127.0.0.1:3080/',
});
const table = renderStartupTable(rows);
expect(table).not.toContain("'");
expect(table).toContain('| Web 主站');
expect(table).toContain('http://127.0.0.1:3080/');
});
test('仅接受同一工作树、实例、数据库和 data dir 的完整栈', () => {
const root = resolve('fixture-root');
const ready = makeReadyState(root);
expect(
isSharedRootStackReady(ready, {
expectedRepoRoot: root,
expectedDatabase: agcDatabase,
expectedDataDir: agcDataDir,
}),
).toBe(true);
const wrongInstance = structuredClone(ready);
wrongInstance.services.web.instanceId = 'other-instance';
expect(
isSharedRootStackReady(wrongInstance, {
expectedRepoRoot: root,
expectedDatabase: agcDatabase,
expectedDataDir: agcDataDir,
}),
).toBe(false);
const incomplete = structuredClone(ready);
incomplete.services['admin-web'].status = 'idle';
expect(
isSharedRootStackReady(incomplete, {
expectedRepoRoot: root,
expectedDatabase: agcDatabase,
expectedDataDir: agcDataDir,
}),
).toBe(false);
});
test('任一子进程退出时收束另一侧进程树', async () => {
const children: FakeChild[] = [];
const stopCalls: FakeChild[] = [];
let nextPid = 100;
const run = runDevAll([], {
resolveEndpoint: async () => ({
port: 3080,
url: 'http://127.0.0.1:3080/',
}),
spawnProcess: () => {
const child = new EventEmitter() as FakeChild;
child.pid = nextPid;
child.exitCode = null;
child.signalCode = null;
nextPid += 1;
children.push(child);
if (children.length === 2) {
queueMicrotask(() => {
child.exitCode = 1;
child.emit('exit', 1, null);
});
}
return child;
},
waitForRoot: async () => makeReadyState(),
readState: () => null,
waitForAgc: async () => true,
stopProcessTree: async (child) => {
stopCalls.push(child);
if (child?.exitCode == null) {
child.exitCode = 0;
child.emit('exit', 0, null);
}
},
});
await expect(run).resolves.toBe(1);
expect(children).toHaveLength(2);
expect(stopCalls).toEqual([children[1], children[0]]);
});
});
-1
View File
@@ -26,7 +26,6 @@ CHUNK_BYTES = 8 * 1024**2
JOBS = {
"ai-game-creator-shell-rust-lane-1",
"ai-game-creator-shell-rust-lane-2",
"ai-game-creator-shell-rust-smoke",
"ai-game-creator-shell-rust-crates",
"backend-tests",
"native-shell-tests",
-1
View File
@@ -11,7 +11,6 @@ import stat
JOBS = {
"ai-game-creator-shell-rust-lane-1",
"ai-game-creator-shell-rust-lane-2",
"ai-game-creator-shell-rust-smoke",
"ai-game-creator-shell-rust-crates",
"backend-tests",
"native-shell-tests",
-2
View File
@@ -33,7 +33,6 @@ LABEL = re.compile(r'(?m)^(\s*-\s*[\"\x27]?)genarrative-ci:docker://(sha256:[0-9
JOBS = {
"AI game creator shell Rust lane 1/2",
"AI game creator shell Rust lane 2/2",
"AI game creator shell Rust smoke",
"AI game creator shell Rust crates",
"Backend tests", "Native shell tests", "Frontend tests",
"Repository checks", "AI game creator shell web tests",
@@ -41,7 +40,6 @@ JOBS = {
RUST_JOB_IDS = {
"AI game creator shell Rust lane 1/2": "ai-game-creator-shell-rust-lane-1",
"AI game creator shell Rust lane 2/2": "ai-game-creator-shell-rust-lane-2",
"AI game creator shell Rust smoke": "ai-game-creator-shell-rust-smoke",
"AI game creator shell Rust crates": "ai-game-creator-shell-rust-crates",
"Backend tests": "backend-tests",
"Native shell tests": "native-shell-tests",
+2 -17
View File
@@ -63,12 +63,11 @@ const jobNames = [
'ai-game-creator-shell-web-tests',
'ai-game-creator-shell-rust-lane-1',
'ai-game-creator-shell-rust-lane-2',
'ai-game-creator-shell-rust-smoke',
'ai-game-creator-shell-rust-crates',
] as const;
// crate 级测试只跑共享 / 平台 / 编辑器插件的 Cargo manifest,不构建 AGC 壳。
// 壳的 Rust 分片与 agent-run smoke 会执行 build.rs,需要 npm 安装的 Claude Agent SDK。
// 壳的 Rust 分片会执行 build.rs,需要 npm 安装的 Claude Agent SDK。
const jobsWithoutNpmInstall: readonly string[] = [
'ai-game-creator-shell-rust-crates',
];
@@ -92,7 +91,6 @@ const nativeShellGateGroupScripts = {
'agc-rust-shard-2': 'npm run check:native-shells:agc-rust-shard-2',
'agc-rust-shard-3': 'npm run check:native-shells:agc-rust-shard-3',
'agc-rust-shard-4': 'npm run check:native-shells:agc-rust-shard-4',
'agc-rust-smoke': 'npm run check:native-shells:agc-rust-smoke',
release: 'npm run check:native-shells:release',
} as const;
@@ -141,7 +139,6 @@ describe('project CI workflow', () => {
const producers = [
'ai-game-creator-shell-rust-lane-1',
'ai-game-creator-shell-rust-lane-2',
'ai-game-creator-shell-rust-smoke',
'ai-game-creator-shell-rust-crates',
'backend-tests',
'native-shell-tests',
@@ -175,8 +172,6 @@ describe('project CI workflow', () => {
'Run AI game creator shell Rust shard 1/4',
'ai-game-creator-shell-rust-lane-2':
'Run AI game creator shell Rust shard 3/4',
'ai-game-creator-shell-rust-smoke':
'Run AI game creator shell agent-run smoke',
'ai-game-creator-shell-rust-crates':
'Run AI game creator shell shared crate gates',
'backend-tests': 'Run server-rs workspace tests',
@@ -242,9 +237,6 @@ describe('project CI workflow', () => {
'cargo test --locked -p spacetime-module --no-fail-fast --manifest-path server-rs/Cargo.toml --no-run',
);
expect(warmCommands).toContain('cd apps/ai-game-creator-shell/src-tauri');
expect(warmCommands).toContain(
'cd apps/ai-game-creator-shell cargo build --manifest-path src-tauri/Cargo.toml',
);
expect(warmCommands).toContain(
'cargo test --manifest-path apps/desktop-shell/src-tauri/Cargo.toml --no-run',
);
@@ -678,13 +670,6 @@ describe('project CI workflow', () => {
nativeShellGateScript.match(/ai-game-creator-shell:check:rust:shell/gu),
).toHaveLength(4);
const smokeJob = jobSection('ai-game-creator-shell-rust-smoke');
expect(smokeJob).toContain(
'run: npm run check:native-shells:agc-rust-smoke',
);
expect(smokeJob).toContain('cargo fetch --locked');
expect(smokeJob).not.toContain('server-rs/Cargo.toml');
const cratesJob = jobSection('ai-game-creator-shell-rust-crates');
expect(cratesJob).toContain(
'run: npm run check:native-shells:agc-rust-crates',
@@ -704,7 +689,7 @@ describe('project CI workflow', () => {
// 拆开的 web / rust 两段必须还是原 `ai-game-creator-shell:check` 的同一条命令序列,
// rust 段再拆成 crate 级与壳分片两段后在聚合脚本里保持同序。
expect(rootPackageJson.scripts?.['ai-game-creator-shell:check']).toBe(
'npm run ai-game-creator-shell:check:web && npm run ai-game-creator-shell:check:rust && npm run ai-game-creator-shell:agent-run:smoke',
'npm run ai-game-creator-shell:check:web && npm run ai-game-creator-shell:check:rust',
);
expect(rootPackageJson.scripts?.['ai-game-creator-shell:check:web']).toBe(
'npm run ai-game-creator-shell:typecheck && npm --prefix apps/ai-game-creator-shell run bundled-resources:test && npm run test -- apps/ai-game-creator-shell/tests',
+2 -2
View File
@@ -340,7 +340,7 @@ class GiteaCacheMaintenanceTest(unittest.TestCase):
instance, run, jobs, artifacts = self.source_fixture()
result = self.select_source(instance)
self.assertEqual(result["run_id"], 44)
self.assertEqual(len(result["exports"]), 6)
self.assertEqual(len(result["exports"]), len(maintenance_module.RUST_JOB_IDS))
self.assertEqual({item["size_in_bytes"] for item in result["exports"]}, {10})
run["event"] = "pull_request"
self.assertIsNone(self.select_source(instance))
@@ -421,7 +421,7 @@ class GiteaCacheMaintenanceTest(unittest.TestCase):
instance.docker = docker
instance.api.download = lambda path, destination, expected_size: destination.write_bytes(b"download")
def merge(inputs, output, **kwargs):
self.assertEqual(len(inputs), 6)
self.assertEqual(len(inputs), len(maintenance_module.RUST_JOB_IDS))
self.assertEqual(kwargs["expected_inherited_source_sha"], "b" * 40)
output.mkdir()
(output / "objects").mkdir()