添加Agent Swarm一键测试入口
新增 agc:test 确定性自动验收短命令。 新增 agc:test:chat 自动发现 AppData、创建测试项目并启动自主 Swarm。 复用正式预览服务完成浏览器打开、信号停止和安全清理。 补充命令契约、跨平台目录、项目哨兵和预览地址测试。 同步客户端技术方案与团队复验流程。
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
"agent-task": "node scripts/run-cli-with-config.mjs --agent-task",
|
||||
"chat": "node scripts/run-cli-with-config.mjs --swarm-chat",
|
||||
"swarm": "node scripts/run-cli-with-config.mjs --swarm-chat",
|
||||
"test:chat": "node scripts/agent-swarm-test-chat.mjs",
|
||||
"agent-run": "node scripts/run-cli-with-config.mjs --agent-run",
|
||||
"agent-run:smoke": "node scripts/smoke-agent-run-local-provider.mjs",
|
||||
"agent-runtime:real-e2e": "node scripts/agent-runtime-real-e2e.mjs",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,10 @@ const defaultAppConfig = JSON.parse(
|
||||
const rootPackageConfig = JSON.parse(
|
||||
fs.readFileSync(new URL('../../../package.json', import.meta.url), 'utf8'),
|
||||
);
|
||||
const swarmTestChatSource = fs.readFileSync(
|
||||
new URL('../scripts/agent-swarm-test-chat.mjs', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
const viteConfigSource = fs.readFileSync(
|
||||
new URL('../vite.config.ts', import.meta.url),
|
||||
'utf8',
|
||||
@@ -401,6 +405,43 @@ if (
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
packageConfig.scripts?.['test:chat'] !==
|
||||
'node scripts/agent-swarm-test-chat.mjs'
|
||||
) {
|
||||
throw new Error(
|
||||
'AI game creator shell test:chat must use the one-click Swarm test entry',
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
rootPackageConfig.scripts?.['agc:test'] !==
|
||||
'npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e --'
|
||||
) {
|
||||
throw new Error('agc:test must delegate to the deterministic playable E2E');
|
||||
}
|
||||
|
||||
if (
|
||||
rootPackageConfig.scripts?.['agc:test:chat'] !==
|
||||
'npm --prefix apps/ai-game-creator-shell run test:chat --'
|
||||
) {
|
||||
throw new Error(
|
||||
'agc:test:chat must delegate to the one-click Swarm test entry',
|
||||
);
|
||||
}
|
||||
|
||||
for (const requiredSource of [
|
||||
"export const appIdentifier = 'world.genarrative.ai-game-creator'",
|
||||
"'--swarm-chat'",
|
||||
"'--autonomous-game-build'",
|
||||
"'--preview-serve'",
|
||||
'cleanupSwarmTestProject(project)',
|
||||
]) {
|
||||
if (!swarmTestChatSource.includes(requiredSource)) {
|
||||
throw new Error(`Swarm test entry contract drifted: ${requiredSource}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (tauriConfig.productName !== 'Genarrative AI Game Creator') {
|
||||
throw new Error('AI game creator shell productName drifted');
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ tauri = { version = "2.11.2", features = [] }
|
||||
tauri-plugin-dialog = "2.7.1"
|
||||
tauri-plugin-opener = "2"
|
||||
tempfile = "3"
|
||||
tokio = { version = "1", features = ["io-util", "macros", "process", "rt-multi-thread", "sync", "time"] }
|
||||
tokio = { version = "1", features = ["io-util", "macros", "process", "rt-multi-thread", "signal", "sync", "time"] }
|
||||
url = "2"
|
||||
unicode-normalization = "0.1"
|
||||
zip = { version = "2", default-features = false, features = ["deflate"] }
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
use super::*;
|
||||
|
||||
const PREVIEW_SERVE_USAGE: &str = "用法:--preview-serve <本地项目绝对路径>";
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub(crate) enum CliCommand {
|
||||
LlmStatus,
|
||||
PreviewServe {
|
||||
project_path: PathBuf,
|
||||
},
|
||||
AgentChat {
|
||||
project_path: PathBuf,
|
||||
agent_id: String,
|
||||
@@ -186,6 +191,7 @@ impl CliCommand {
|
||||
| Self::AgentRetry { project_path, .. }
|
||||
| Self::AgentSteer { project_path, .. }
|
||||
| Self::AgentResume { project_path }
|
||||
| Self::PreviewServe { project_path }
|
||||
| Self::AgentRun { project_path, .. } => Some((project_path, false)),
|
||||
Self::LlmStatus | Self::RunnerStatus => None,
|
||||
}
|
||||
@@ -413,6 +419,14 @@ fn parse_cli_agent_goal_revision(value: &str, usage: &str) -> Result<u64, String
|
||||
}
|
||||
|
||||
pub(crate) fn parse_cli_command(args: &[String]) -> Result<Option<CliCommand>, String> {
|
||||
if args.first().map(String::as_str) == Some("--preview-serve") {
|
||||
if args.len() != 2 || args[1].trim().is_empty() {
|
||||
return Err(PREVIEW_SERVE_USAGE.to_string());
|
||||
}
|
||||
return Ok(Some(CliCommand::PreviewServe {
|
||||
project_path: PathBuf::from(&args[1]),
|
||||
}));
|
||||
}
|
||||
if args.first().map(String::as_str) == Some("--llm-status") {
|
||||
return Ok(Some(CliCommand::LlmStatus));
|
||||
}
|
||||
@@ -785,6 +799,35 @@ pub(crate) fn run_cli_command(command: CliCommand) -> Result<(), String> {
|
||||
Err("LLM 配置未就绪".to_string())
|
||||
}
|
||||
}
|
||||
CliCommand::PreviewServe { project_path } => {
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(|error| format!("创建 CLI runtime 失败:{error}"))?;
|
||||
let registry = game_creator_preview_registry();
|
||||
let preview = start_local_game_preview_at(&project_path, ®istry)?;
|
||||
println!("preview.running");
|
||||
println!("projectPath={}", project_path.display());
|
||||
println!("previewUrl={}", preview.url);
|
||||
let wait_result = std::io::stdout()
|
||||
.flush()
|
||||
.map_err(|error| format!("刷新本地预览 CLI 输出失败:{error}"))
|
||||
.and_then(|()| {
|
||||
runtime
|
||||
.block_on(tokio::signal::ctrl_c())
|
||||
.map_err(|error| format!("等待 Ctrl+C 失败:{error}"))
|
||||
});
|
||||
let stop_result = stop_local_game_preview_for_root(Some(&project_path), ®istry)
|
||||
.map(|_| ())
|
||||
.map_err(|error| format!("停止本地预览失败:{error}"));
|
||||
match (wait_result, stop_result) {
|
||||
(Ok(()), Ok(())) => Ok(()),
|
||||
(Err(error), Ok(())) | (Ok(()), Err(error)) => Err(error),
|
||||
(Err(wait_error), Err(stop_error)) => {
|
||||
Err(format!("{wait_error};同时{stop_error}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
CliCommand::AgentChat {
|
||||
project_path,
|
||||
agent_id,
|
||||
@@ -1329,6 +1372,54 @@ mod tests {
|
||||
assert_eq!(parsed["runtimes"][0]["state"]["status"], "running");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_preview_serve_without_runner_or_config() {
|
||||
let project_path = std::env::current_dir().expect("current directory");
|
||||
let mut command = parse_cli_command(&[
|
||||
"--preview-serve".to_string(),
|
||||
project_path.display().to_string(),
|
||||
])
|
||||
.expect("parse preview serve")
|
||||
.expect("preview serve command");
|
||||
|
||||
assert_eq!(
|
||||
command,
|
||||
CliCommand::PreviewServe {
|
||||
project_path: project_path.clone(),
|
||||
}
|
||||
);
|
||||
assert!(!command.requires_external_agent_runner());
|
||||
assert!(!command.requires_started_external_agent_runner());
|
||||
assert!(!command.is_read_only_status());
|
||||
assert_eq!(
|
||||
prepare_cli_command_paths(&mut command, None)
|
||||
.expect("prepare preview serve without config dir"),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
command,
|
||||
CliCommand::PreviewServe {
|
||||
project_path: fs::canonicalize(project_path).expect("canonical project path"),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_serve_requires_exactly_one_non_empty_project_path() {
|
||||
for args in [
|
||||
vec!["--preview-serve"],
|
||||
vec!["--preview-serve", " "],
|
||||
vec!["--preview-serve", "/tmp/game-project", "/tmp/extra"],
|
||||
] {
|
||||
let args = args.into_iter().map(str::to_string).collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
parse_cli_command(&args),
|
||||
Err(PREVIEW_SERVE_USAGE.to_string()),
|
||||
"unexpected parse result for {args:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_agent_steer_with_stdin_only_contract() {
|
||||
let command = parse_cli_command(&[
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
import {
|
||||
lstat,
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
readdir,
|
||||
readFile,
|
||||
realpath,
|
||||
rm,
|
||||
symlink,
|
||||
writeFile,
|
||||
} from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
appIdentifier,
|
||||
buildCargoCliArguments,
|
||||
cleanupSwarmTestProject,
|
||||
configFileName,
|
||||
defaultRuntimeConfigDirCandidates,
|
||||
discoverRuntimeConfigDir,
|
||||
parseSwarmTestArguments,
|
||||
prepareSwarmTestProject,
|
||||
testProjectPrefix,
|
||||
testProjectSentinelName,
|
||||
testProjectSentinelSchema,
|
||||
validatePreviewUrl,
|
||||
} from '../scripts/agent-swarm-test-chat.mjs';
|
||||
|
||||
const appRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
|
||||
|
||||
async function withTemporaryRoot<T>(
|
||||
run: (root: string) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const root = await mkdtemp(
|
||||
path.join(os.tmpdir(), 'genarrative-swarm-entry-test-'),
|
||||
);
|
||||
try {
|
||||
return await run(root);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function pathExists(targetPath: string): Promise<boolean> {
|
||||
return lstat(targetPath).then(
|
||||
() => true,
|
||||
(error: NodeJS.ErrnoException) => {
|
||||
if (error.code === 'ENOENT') return false;
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
describe('Swarm test argument parsing', () => {
|
||||
it('returns the documented defaults', () => {
|
||||
expect(parseSwarmTestArguments([])).toEqual({
|
||||
configDir: null,
|
||||
projectDir: null,
|
||||
keepProject: false,
|
||||
openBrowser: true,
|
||||
dryRun: false,
|
||||
help: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('parses every supported option', () => {
|
||||
const configDir = path.resolve('fixture-config');
|
||||
const projectDir = path.resolve('fixture-project');
|
||||
|
||||
expect(
|
||||
parseSwarmTestArguments([
|
||||
'--config-dir',
|
||||
configDir,
|
||||
'--project-dir',
|
||||
projectDir,
|
||||
'--keep-project',
|
||||
'--no-open',
|
||||
'--dry-run',
|
||||
'--help',
|
||||
]),
|
||||
).toEqual({
|
||||
configDir,
|
||||
projectDir,
|
||||
keepProject: true,
|
||||
openBrowser: false,
|
||||
dryRun: true,
|
||||
help: true,
|
||||
});
|
||||
expect(parseSwarmTestArguments(['-h']).help).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: 'duplicate config directory',
|
||||
args: ['--config-dir', '/first', '--config-dir', '/second'],
|
||||
marker: '--config-dir',
|
||||
},
|
||||
{
|
||||
label: 'duplicate project directory',
|
||||
args: ['--project-dir', '/first', '--project-dir', '/second'],
|
||||
marker: '--project-dir',
|
||||
},
|
||||
{
|
||||
label: 'missing config directory',
|
||||
args: ['--config-dir'],
|
||||
marker: '--config-dir',
|
||||
},
|
||||
{
|
||||
label: 'missing project directory',
|
||||
args: ['--project-dir', '--dry-run'],
|
||||
marker: '--project-dir',
|
||||
},
|
||||
{
|
||||
label: 'unknown option',
|
||||
args: ['--unsupported'],
|
||||
marker: '--unsupported',
|
||||
},
|
||||
])('rejects $label', ({ args, marker }) => {
|
||||
expect(() => parseSwarmTestArguments(args)).toThrow(marker);
|
||||
});
|
||||
});
|
||||
|
||||
describe('runtime config directory candidates', () => {
|
||||
it('uses an absolute Linux XDG config root', () => {
|
||||
expect(
|
||||
defaultRuntimeConfigDirCandidates({
|
||||
platform: 'linux',
|
||||
environment: { XDG_CONFIG_HOME: '/fixture/xdg' },
|
||||
homeDirectory: '/fixture/home',
|
||||
}),
|
||||
).toEqual([path.posix.join('/fixture/xdg', appIdentifier)]);
|
||||
});
|
||||
|
||||
it('falls back to the Linux home config root', () => {
|
||||
expect(
|
||||
defaultRuntimeConfigDirCandidates({
|
||||
platform: 'linux',
|
||||
environment: { XDG_CONFIG_HOME: 'relative-xdg' },
|
||||
homeDirectory: '/fixture/home',
|
||||
}),
|
||||
).toEqual([path.posix.join('/fixture/home', '.config', appIdentifier)]);
|
||||
});
|
||||
|
||||
it('uses the macOS Application Support directory', () => {
|
||||
expect(
|
||||
defaultRuntimeConfigDirCandidates({
|
||||
platform: 'darwin',
|
||||
environment: {},
|
||||
homeDirectory: '/Users/fixture',
|
||||
}),
|
||||
).toEqual([
|
||||
path.posix.join(
|
||||
'/Users/fixture',
|
||||
'Library',
|
||||
'Application Support',
|
||||
appIdentifier,
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses both Windows roaming and local AppData directories', () => {
|
||||
const appData = 'C:\\Users\\fixture\\AppData\\Roaming';
|
||||
const localAppData = 'C:\\Users\\fixture\\AppData\\Local';
|
||||
|
||||
expect(
|
||||
defaultRuntimeConfigDirCandidates({
|
||||
platform: 'win32',
|
||||
environment: {
|
||||
APPDATA: appData,
|
||||
LOCALAPPDATA: localAppData,
|
||||
},
|
||||
homeDirectory: 'C:\\Users\\fixture',
|
||||
}),
|
||||
).toEqual([
|
||||
path.win32.join(appData, appIdentifier),
|
||||
path.win32.join(localAppData, appIdentifier),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('runtime config discovery', () => {
|
||||
it('discovers an explicitly selected config directory', async () => {
|
||||
await withTemporaryRoot(async (root) => {
|
||||
const configDir = path.join(root, 'explicit-config');
|
||||
await mkdir(configDir);
|
||||
await writeFile(path.join(configDir, configFileName), '{}\n');
|
||||
|
||||
await expect(
|
||||
discoverRuntimeConfigDir(configDir, {
|
||||
platform: 'linux',
|
||||
environment: { XDG_CONFIG_HOME: path.join(root, 'unused') },
|
||||
homeDirectory: path.join(root, 'unused-home'),
|
||||
}),
|
||||
).resolves.toBe(await realpath(configDir));
|
||||
});
|
||||
});
|
||||
|
||||
it('discovers a config directory from an isolated XDG root', async () => {
|
||||
await withTemporaryRoot(async (root) => {
|
||||
const xdgRoot = path.join(root, 'xdg');
|
||||
const configDir = path.join(xdgRoot, appIdentifier);
|
||||
await mkdir(configDir, { recursive: true });
|
||||
await writeFile(path.join(configDir, configFileName), '{}\n');
|
||||
|
||||
await expect(
|
||||
discoverRuntimeConfigDir(null, {
|
||||
platform: 'linux',
|
||||
environment: { XDG_CONFIG_HOME: xdgRoot },
|
||||
homeDirectory: path.join(root, 'unused-home'),
|
||||
}),
|
||||
).resolves.toBe(await realpath(configDir));
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
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,
|
||||
});
|
||||
await expect(
|
||||
discoverRuntimeConfigDir(directoryConfigDir),
|
||||
).rejects.toThrow(configFileName);
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects a relative explicit config directory', async () => {
|
||||
await expect(discoverRuntimeConfigDir('relative-config')).rejects.toThrow(
|
||||
'--config-dir',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Swarm test project ownership', () => {
|
||||
it('creates a sentinel-owned project and removes it during cleanup', async () => {
|
||||
await withTemporaryRoot(async (root) => {
|
||||
const project = await prepareSwarmTestProject(null, root);
|
||||
const sentinelPath = path.join(project.path, testProjectSentinelName);
|
||||
const sentinelMetadata = await lstat(sentinelPath);
|
||||
const sentinel = JSON.parse(await readFile(sentinelPath, 'utf8'));
|
||||
|
||||
expect(project.owned).toBe(true);
|
||||
expect(project.sentinelToken).toEqual(expect.any(String));
|
||||
expect(path.basename(project.path)).toMatch(
|
||||
new RegExp(`^${testProjectPrefix}`),
|
||||
);
|
||||
expect(sentinelMetadata.isFile()).toBe(true);
|
||||
expect(sentinelMetadata.isSymbolicLink()).toBe(false);
|
||||
expect(sentinel).toEqual({
|
||||
schemaVersion: testProjectSentinelSchema,
|
||||
token: project.sentinelToken,
|
||||
});
|
||||
|
||||
await expect(cleanupSwarmTestProject(project)).resolves.toBe(true);
|
||||
expect(await pathExists(project.path)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses cleanup after the sentinel identity is changed', async () => {
|
||||
await withTemporaryRoot(async (root) => {
|
||||
const project = await prepareSwarmTestProject(null, root);
|
||||
await writeFile(
|
||||
path.join(project.path, testProjectSentinelName),
|
||||
`${JSON.stringify({
|
||||
schemaVersion: testProjectSentinelSchema,
|
||||
token: 'changed-token',
|
||||
})}\n`,
|
||||
);
|
||||
|
||||
await expect(cleanupSwarmTestProject(project)).rejects.toThrowError();
|
||||
expect(await pathExists(project.path)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps an explicit empty directory unowned', async () => {
|
||||
await withTemporaryRoot(async (root) => {
|
||||
const explicitProjectDir = path.join(root, 'explicit-project');
|
||||
await mkdir(explicitProjectDir);
|
||||
|
||||
const project = await prepareSwarmTestProject(explicitProjectDir, root);
|
||||
|
||||
expect(project).toEqual({
|
||||
path: await realpath(explicitProjectDir),
|
||||
owned: false,
|
||||
sentinelToken: null,
|
||||
});
|
||||
await expect(cleanupSwarmTestProject(project)).resolves.toBe(false);
|
||||
expect(await readdir(explicitProjectDir)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects a non-empty uninitialized explicit directory', async () => {
|
||||
await withTemporaryRoot(async (root) => {
|
||||
const explicitProjectDir = path.join(root, 'uninitialized-project');
|
||||
await mkdir(explicitProjectDir);
|
||||
await writeFile(path.join(explicitProjectDir, 'existing.txt'), 'fixture');
|
||||
|
||||
await expect(
|
||||
prepareSwarmTestProject(explicitProjectDir, root),
|
||||
).rejects.toThrow('--project-dir');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cargo CLI argument construction', () => {
|
||||
it('uses the shell manifest and separates cargo from application arguments', () => {
|
||||
const cliArguments = ['--config-dir', 'fixture-config', '--llm-status'];
|
||||
const cargoArguments = buildCargoCliArguments(cliArguments);
|
||||
|
||||
expect(cargoArguments.slice(0, 2)).toEqual(['run', '--manifest-path']);
|
||||
expect(path.isAbsolute(cargoArguments[2])).toBe(true);
|
||||
expect(path.relative(appRoot, cargoArguments[2])).toBe(
|
||||
path.join('src-tauri', 'Cargo.toml'),
|
||||
);
|
||||
expect(cargoArguments[3]).toBe('--');
|
||||
expect(cargoArguments.slice(4)).toEqual(cliArguments);
|
||||
});
|
||||
});
|
||||
|
||||
describe('preview URL validation', () => {
|
||||
it('accepts an HTTP URL on the numeric loopback host', () => {
|
||||
const previewUrl = 'http://127.0.0.1:4173/';
|
||||
|
||||
expect(validatePreviewUrl(previewUrl)).toBe(previewUrl);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'https://127.0.0.1:4173/',
|
||||
'http://localhost:4173/',
|
||||
'http://[::1]:4173/',
|
||||
'http://192.0.2.1:4173/',
|
||||
'http://127.0.0.1.example:4173/',
|
||||
'http://127.0.0.1/',
|
||||
'http://127.0.0.1:4173/play',
|
||||
'http://127.0.0.1:4173/?mode=test',
|
||||
'http://127.0.0.1:4173/#ready',
|
||||
'http://user@127.0.0.1:4173/',
|
||||
'file:///fixture/game/index.html',
|
||||
'not-a-url',
|
||||
])('rejects %s', (previewUrl) => {
|
||||
expect(() => validatePreviewUrl(previewUrl)).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('package script registration', () => {
|
||||
it('registers the root and app test commands exactly', async () => {
|
||||
const [rootPackage, appPackage] = await Promise.all(
|
||||
[
|
||||
new URL('../../../package.json', import.meta.url),
|
||||
new URL('../package.json', import.meta.url),
|
||||
].map(async (packageUrl) =>
|
||||
JSON.parse(await readFile(packageUrl, 'utf8')),
|
||||
),
|
||||
);
|
||||
|
||||
expect(rootPackage.scripts?.['agc:test']).toBe(
|
||||
'npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e --',
|
||||
);
|
||||
expect(rootPackage.scripts?.['agc:test:chat']).toBe(
|
||||
'npm --prefix apps/ai-game-creator-shell run test:chat --',
|
||||
);
|
||||
expect(appPackage.scripts?.['test:chat']).toBe(
|
||||
'node scripts/agent-swarm-test-chat.mjs',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -161,6 +161,15 @@ suite 只能读正式 AppData,在其同级目录写入 sentinel 管理的 `060
|
||||
|
||||
### AI 游戏创作自主 Swarm 终端复验
|
||||
|
||||
日常人工验收优先使用短入口,不再手工拼 AppData、临时项目和预览命令:
|
||||
|
||||
```bash
|
||||
npm run agc:test
|
||||
npm run agc:test:chat
|
||||
```
|
||||
|
||||
`agc:test` 委托确定性 lane-defense E2E,使用本地 loopback Provider 完成 Runtime、项目写入和真实浏览器 `37/37` 门禁,不消耗外部 Provider。`agc:test:chat` 按当前平台自动查找发布客户端 AppData 中的 `game-creator.config.json`,创建 sentinel 管理的一次性项目,进入 `project-supervisor + autonomous-game-build`;用户只输入一条需求并以 EOF 交付,收束后复用正式 localhost preview server 并打开试玩。正常收束后按 `Ctrl+C` 默认清理一次性项目;尚有后台任务或启动预览失败时保留现场,避免误删运行中项目。需要主动保留时显式追加 `-- --keep-project`,需要覆盖配置或项目时使用 `--config-dir` / `--project-dir` 绝对路径。脚本不得读取或打印 API Key,显式项目永不自动删除,非空且未初始化目录必须拒绝。
|
||||
|
||||
修改 Supervisor 自主编排、`agent.message`、static delivery/claim/repair、Swarm CLI `turn.report`、Runner 恢复或 autonomous harness 后,先跑确定性收敛门禁,再运行真实终端 suite:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -86,6 +86,8 @@ V1.11 的受保护仓库控制目录同时包含 `.git / .agent / .agents / .cod
|
||||
|
||||
V1.47 在只读工具边界和 batch v3/v2/v1 恢复终审修复后的最新独立真实外部轮次已完整 **PASS**:用户只输入一次任务后 stdin 立即 EOF,人工 approve / answer / steer 均为 `0`;一个原始专业任务失败后由唯一 repair 自行恢复,父 Supervisor 为 `idle / completed`,`turn.report=settled` 且只有 `1` 条 `44` 字符 assistant。项目 revision `0 -> 6`,`game/index.html` 为 `8080` bytes 且已变化,两次静态检查通过,desktop / mobile 的 `lane-defense-v1` 真实 Chrome 试玩为 `37/37`。`88` 个 Provider identity 全部 terminal,其中 `75 completed / 13 failed`,`12` 条 durable retry audit 与专业 repair 均自行恢复;open lifecycle、pending、confirmation、user-input、provider batch/retry/handoff/tool-plan handoff、finalization、reconciliation、duplicate 与各类泄漏终局均为 `0`,Runner、disposable 项目和隔离 AppData 已自动清理。该轮证明失败 attempt 可保留真实证据而循环仍能零人工干预收束,不能把它改写成 Provider 零失败。
|
||||
|
||||
2026-07-23 起,开发验收提供两个根级短入口。`npm run agc:test` 直接委托现有确定性可玩塔防 E2E,不复制 Runtime harness;`npm run agc:test:chat` 自动发现 Tauri identifier `world.genarrative.ai-game-creator` 对应 AppData 中的 `game-creator.config.json`,创建带私有 sentinel 的一次性项目,先执行不回显密钥的 LLM 状态检查,再以 `--swarm-chat --init --autonomous-game-build` 进入真实 Project Supervisor。用户只输入需求并发送 EOF;正常收束且存在 `game/index.html` 后,通过仅开发 CLI `--preview-serve` 复用正式 localhost preview server,自动打开固定形态的 loopback 试玩地址。预览按 `Ctrl+C` 结束后默认验证 sentinel 并清理项目;尚未收束或预览启动失败时保留现场,避免删除后台任务和失败证据。`--keep-project` 可主动保留,显式 `--project-dir` 永不删除,非空未初始化目录拒绝,`--config-dir` / `--project-dir` 只接受绝对路径。该人工入口用于快速体验,不能替代真实外部 Provider E2E 的完整生命周期、隐私和残留门禁。
|
||||
|
||||
2026-07-15 起,Runtime V1.1 文档的“V1.17 单 Agent 持久计划”作为后台工具规划进度的新事实源。`submit_agent_tool_plan` 新增 nullable `planUpdate={explanation,steps[{step,status}]}`;步骤只接受 `pending / in_progress / completed`,最多 8 步且至多一个 `in_progress`。结构化计划一旦建立,legacy `plan` 只作旧协议 fallback;终态步骤必须保留,`planRevision` 只在真实变化时单调递增,工具 action 下标不得自动完成结构化步骤,存在未完成步骤时不得写最终回复或 completed。
|
||||
|
||||
V1.17 计划快照随 `game-creator-runtime-context-bundle.v3` 持久化,v2 在通过原身份、revision 和 verification gate 校验后从当前 Runtime state 补齐计划字段继续恢复;计划元数据本身不推进项目 revision、不改变 verification gate,也不触发项目权限确认。开发 UI 和 CLI 有界展示 revision、说明与完整 8 步;正式用户的 Supervisor 只展示完成数、当前步骤、等待对象、下一步和协作数量的紧凑摘要。恢复、same-run steer 和真实 Provider 的完整验收矩阵以 Runtime V1.17 章节为准;2026-07-16 已在当前 v5 context 上完成正式 `openai_chat / gpt-5.5` 的同 run steer + Runner 强杀恢复专项,门禁状态为 PASS。
|
||||
|
||||
@@ -134,6 +134,8 @@
|
||||
"agc:build": "npm --prefix apps/ai-game-creator-shell run build --",
|
||||
"agc:check": "npm run ai-game-creator-shell:check",
|
||||
"agc:typecheck": "npm --prefix apps/ai-game-creator-shell run typecheck",
|
||||
"agc:test": "npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e --",
|
||||
"agc:test:chat": "npm --prefix apps/ai-game-creator-shell run test:chat --",
|
||||
"ai-game-creator-shell:dev": "npm --prefix apps/ai-game-creator-shell run dev",
|
||||
"ai-game-creator-shell:dev-server": "npm --prefix apps/ai-game-creator-shell run dev-server",
|
||||
"ai-game-creator-shell:build": "npm --prefix apps/ai-game-creator-shell run build --",
|
||||
|
||||
Reference in New Issue
Block a user