feat: release Dever 0.1.4 with enforceable project guidance

This commit is contained in:
Antigravity committed 2026-10-09 05:33:57 +00:00
1 parent 201bdee372
commit 89629c2197
86 files changed
+2798 -338

No files matched your search

+4 -2
View File
@@ -68,11 +68,13 @@ python3 -B test/ci/run.py run --stage sandbox
python3 -B test/ci/run.py run --stage bounded-perf
```
以上命令由同一受限托管进程串行执行。若 author 改变输入,应在作者准备完成后再跑正式静态/default/all-feature门;较早输入版本的证据会显示过期。每阶段写入新的独占时间目录,开始即记录 running;缺输入记录 blocked,失败命令保留 exit/count/日志与原始异常。`summary` 与 inventory 消费者只接受最新一次尝试的当前源码/输入证据,不能越过较新的失败或中断回用旧 pass;所有必选阶段通过才返回0。源码摘要覆盖未提交文件,不只看 Git HEAD。
以上命令由同一受限托管进程串行执行。若 author 改变输入,应在作者准备完成后再跑正式静态/default/all-feature门;较早输入版本的证据会显示过期。每阶段写入新的独占时间目录,开始即记录 running;缺输入记录 blocked,失败命令保留 exit/count/日志与原始异常。`summary` 与 inventory 消费者只接受最新一次尝试的当前源码/输入证据,不能越过较新的失败或中断回用旧 pass;所有必选阶段通过才返回0。源码摘要覆盖未提交文件及 `skills/` 的指引、模板和示例,不只看 Git HEAD;修改或删除这些文件会使旧结果失效。
default/all-features 由实际 Cargo metadata 注册 target 和已解析 feature 形成串行矩阵,覆盖 lib/bin/integration/doctest;default 显式保留 workspace 解析的 package features,最小 feature 阶段另按单 package 验证。只有显式启用 `remove_test_executables` 时才删除 Cargo 本次返回、`profile.test=true` 且位于 target 下 `deps/` 的测试可执行文件,失败阶段同样清理;不删除作者工具、静态 init、依赖 archive 或整个 target。删除对象在日志 JSON 中可审计,重跑 Cargo 可恢复。inventory 按 package/target/name 三元组核对完整性,跨 target 同名用例不能代替缺失项。
Python 阶段包含 `test/ci`、`test/performance`、`test/ecosystem-release` 三个目录;后两者必须已配置 `performance.network_peer/live_peer`,真实自有 loopback检查由主代理执行。三 SDK 路径全部必需,三个明确成功标志全部出现才通过。
Python 阶段包含 `test/ci`、`test/performance`、`test/ecosystem-release` 三个目录,并执行 `test/skill_examples.py`,检查 skill 安装后的本地链接、文件预算、标准库签名和教程代码与源码的一致性。教程代码块须链接到受检查项目中的唯一源码片段;文档或源码漂移都会失败。performance 测试必须已配置 `performance.network_peer/live_peer`,真实自有 loopback 检查由主代理执行。三 SDK 路径全部必需,三个明确成功标志全部出现才通过。
只验证本次 CI 或 skill 改动时,可运行 `python3 -B test/ci/test_gate.py -v` 和 `python3 -B test/skill_examples.py -v`,不需要启动完整 CI、数据库或压力测试。
PostgreSQL 只接受显式 loopback端口和恰好一个 `{case}` 库名模板,拒绝 query 覆盖 host/dbname 等。增加 `performance.linux_ci.owned_postgres: {"data_directory":"/absolute/owned-cluster","port":51234}`,由托管者创建所需基库并在 finally 停止该自有实例;gate复用现有用例的 schema/tenant 清理,不替用户启动或连接既有业务数据库。
+3 -2
View File
@@ -20,7 +20,7 @@ STAGES = ("static", "default", "all-features", "features", "python", "sdk",
"inventory", "author", "native", "postgres", "sandbox", "bounded-perf",
"cms-64", "cms-128", "http2-64", "http2-128", "recovery-64", "recovery-128",
"live-64", "live-128")
SOURCE_ROOTS = ("crates", "test", "sdk", "library", "examples", ".cargo")
SOURCE_ROOTS = ("crates", "test", "sdk", "library", "examples", "skills", ".cargo")
class MissingInput(Exception):
@@ -129,7 +129,8 @@ def command_plan(stage, config):
python = str(absolute_file(tools.get("python"), "performance.linux_ci.tools.python"))
if stage == "python":
return [[python, "-B", "-m", "unittest", "discover", "-s", directory, "-p", "test_*.py", "-v"]
for directory in ("test/ci", "test/performance", "test/ecosystem-release")]
for directory in ("test/ci", "test/performance", "test/ecosystem-release")] + [
[python, "-B", "test/skill_examples.py", "-v"]]
if stage == "sdk":
return [[python, "-B", "test/external-sdk/check.py", "--python", python,
"--node", str(absolute_file(tools.get("node"), "performance.linux_ci.tools.node")),
+76
View File
@@ -5,6 +5,7 @@ import contextlib
import io
import json
from pathlib import Path
import shutil
import sys
import tempfile
import unittest
@@ -177,6 +178,81 @@ class GateTests(unittest.TestCase):
(root / "Cargo.lock").write_text("changed immutable input")
self.assertNotEqual(author_before, gate.input_fingerprint({}, config, "author"))
def test_skill_sources_invalidate_evidence_but_generated_caches_do_not(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
for name in ("Cargo.toml", "Cargo.lock"):
(root / name).write_text(name)
sources = ("skills/assets/template/AGENTS.md", "skills/references/development.md",
"skills/assets/examples/jobs/module/report/archive/app.dever")
for name in sources:
path = root / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("original")
original = gate.source_fingerprint(root)
for name in sources:
path = root / name
with self.subTest(source=name):
path.write_text("changed")
self.assertNotEqual(original, gate.source_fingerprint(root))
path.unlink()
self.assertNotEqual(original, gate.source_fingerprint(root))
path.write_text("original")
self.assertEqual(original, gate.source_fingerprint(root))
for cache in ("__pycache__", "target", ".git"):
path = root / "skills/assets/examples/jobs" / cache / "generated"
path.parent.mkdir()
path.write_text("generated bytes")
self.assertEqual(original, gate.source_fingerprint(root))
path.write_text("changed generated bytes")
self.assertEqual(original, gate.source_fingerprint(root))
path.unlink()
self.assertEqual(original, gate.source_fingerprint(root))
def test_python_plan_runs_real_skill_checks_and_rejects_each_failure(self):
config = {"tools": {"cargo": sys.executable, "python": sys.executable}}
commands = gate.command_plan("python", config)
skill_commands = [command for command in commands if "test/skill_examples.py" in command]
self.assertEqual(len(skill_commands), 1)
command = skill_commands[0]
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
(root / "test").mkdir()
shutil.copyfile(gate.ROOT / "test/skill_examples.py", root / "test/skill_examples.py")
for name in ("skills", "library"):
shutil.copytree(gate.ROOT / name, root / name)
logs = root / "logs"
logs.mkdir()
with patch.object(gate, "ROOT", root):
record, content = gate.run_command(command, logs, 0, 10)
gate.check_output("python", record, content)
test_count = record["python_tests"]
self.assertGreater(test_count, 0)
for number, (relative, broken, case) in enumerate((
("skills/SKILL.md", "[broken](missing.md)",
"test_relocated_skill_links_and_release_file_limits"),
("skills/references/signatures.md", "```signatures\ndever.io.missing() ()\n```\n",
"test_signature_index_matches_public_library_declarations"),
("skills/references/language.md",
(root / "skills/references/language.md").read_text().replace(
'"Hello, "', '"Incorrect tutorial, "', 1
),
"test_tutorial_code_matches_relocated_checked_sources"),
), 1):
path = root / relative
original = path.read_bytes()
path.write_text(broken)
with self.subTest(check=case), self.assertRaisesRegex(RuntimeError, "command exited 1"):
gate.run_command(command, logs, number, 10)
result = json.loads((logs / f"{number:03d}.command.json").read_text())
self.assertEqual(result["exit_code"], 1)
self.assertEqual(result["python_tests"], test_count)
log = (logs / f"{number:03d}.log").read_text()
self.assertTrue(
f"FAIL: {case}" in log or f"ERROR: {case}" in log, log
)
path.write_bytes(original)
def test_cleanup_cannot_delete_author_binary(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
+182
View File
@@ -151,6 +151,12 @@ fn application_check_rejects_redundancy_before_preparing_build_resources() {
] {
assert!(diagnostic.contains(expected), "{expected}: {diagnostic}");
}
assert_eq!(diagnostic.matches("error[C016]").count(), 3, "{diagnostic}");
assert!(!diagnostic.contains("warning[W005]"), "{diagnostic}");
assert!(
diagnostic.contains("sample/profile/domain.dever:1:1: note:"),
"{diagnostic}"
);
// The same source fails before native packs, output publication or execution.
let executable = root.join("must-not-build");
for (action, args) in [
@@ -166,6 +172,120 @@ fn application_check_rejects_redundancy_before_preparing_build_resources() {
assert!(!toolchain.path().join("cache").exists());
}
#[test]
fn application_check_rejects_proven_duplicates_before_runtime_or_database_access() {
let toolchain = TemporaryDirectory::new();
let compiler = compiler(toolchain.path(), "dever");
for (domain, app, diagnostic, location, warning) in [
(
"first(value: Int) (answer: Int) pure { answer = value + 1 }\nsecond(renamed: Int) (answer: Int) pure { answer = renamed + 1 }",
"total(value: Int) (answer: Int) { first = domain.first(value)\nsecond = domain.second(value)\nanswer = first + second }",
"same implementation and contract",
"sample/count/domain.dever:2:1: error[C016]",
"warning[W003]",
),
(
"calculate(value: Int) (answer: Int) pure { answer = value + 1 }",
"total(value: Int) (answer: Int) { first = domain.calculate(value)\nsecond = domain.calculate(value)\nanswer = first + second }",
"earlier result is still available",
"sample/count/app.dever:2:",
"warning[W004]",
),
] {
let project_dir = TemporaryDirectory::new();
let root = project_dir.path();
write(root, "config/setting.json", "{}");
write(root, "module/sample/count/model.dever", "type Count { value: Int }");
write(root, "module/sample/count/domain.dever", domain);
write(root, "module/sample/count/app.dever", app);
write(root, "module/sample/count/api.dever", "cmd total = app.total");
let checked = cli(&compiler, root, "check", &[]);
for expected in ["C016", diagnostic, location, "previous implementation or computation"] {
failure(&checked, expected);
}
let stderr = String::from_utf8_lossy(&checked.stderr);
assert_eq!(stderr.matches("error[C016]").count(), 1, "{stderr}");
assert!(!stderr.contains(warning), "{stderr}");
assert_eq!(
stderr.matches("note: previous implementation or computation").count(),
1,
"{stderr}"
);
let executable = root.join("must-not-build");
for (action, arguments) in [
("run", vec!["--", "sample.count.total", "{\"value\":1}"]),
("build", vec!["--output", executable.to_str().unwrap()]),
] {
let rejected = cli(&compiler, root, action, &arguments);
assert!(!rejected.status.success());
assert_eq!(rejected.stderr, checked.stderr);
}
let snapshot = cli(&compiler, root, "api", &[]);
success(&snapshot);
let stderr = String::from_utf8_lossy(&snapshot.stderr);
assert!(stderr.contains(warning), "{stderr}");
assert!(!stderr.contains("C016"), "{stderr}");
write(
root,
"test/sample/count/value.dever",
"value() () { assert_eq(app.total(1), 4) }",
);
let tests = cli(&compiler, root, "test", &[]);
failure(&tests, "runtime pack");
let stderr = String::from_utf8_lossy(&tests.stderr);
assert!(stderr.contains(warning), "{stderr}");
assert!(!stderr.contains("C016"), "{stderr}");
write(root, "dever.api", "invalid baseline");
let baseline = cli(&compiler, root, "check", &[]);
failure(&baseline, "invalid API baseline");
let stderr = String::from_utf8_lossy(&baseline.stderr);
assert!(stderr.contains(warning), "{stderr}");
assert!(!stderr.contains("C016"), "{stderr}");
assert!(!executable.exists());
assert!(!root.join("data").exists());
}
assert!(!toolchain.path().join("cache").exists());
}
#[test]
fn application_check_keeps_warnings_with_distinct_evidence_and_other_codes() {
let toolchain = TemporaryDirectory::new();
let compiler = compiler(toolchain.path(), "dever");
let project_dir = TemporaryDirectory::new();
let root = project_dir.path();
write(root, "config/setting.json", "{}");
write(
root,
"module/sample/count/domain.dever",
"orphan(value: Int) (answer: Int) pure { answer = value + 1 }\nfirst(value: Int) (answer: Int) pure { answer = value + 1 }\nsecond(value: Int) (answer: Int) pure { answer = value + 1 }",
);
write(
root,
"module/sample/count/app.dever",
"total(value: Int) (answer: Int) { unused = 0\nfirst = domain.first(value)\nsecond = domain.second(value)\nanswer = first + second }",
);
write(root, "module/sample/count/api.dever", "cmd total = app.total");
let checked = cli(&compiler, root, "check", &[]);
failure(&checked, "C016");
let stderr = String::from_utf8_lossy(&checked.stderr);
assert_eq!(stderr.matches("error[C016]").count(), 2, "{stderr}");
// Warning proof includes the orphan; production proof starts at the first reachable function.
assert_eq!(stderr.matches("warning[W003]").count(), 2, "{stderr}");
assert_eq!(stderr.matches("warning[W001]").count(), 1, "{stderr}");
assert_eq!(
stderr.matches("sample/count/domain.dever:1:1: note:").count(),
2,
"{stderr}"
);
assert_eq!(
stderr.matches("sample/count/domain.dever:2:1: note:").count(),
1,
"{stderr}"
);
assert!(!root.join("data").exists());
assert!(!toolchain.path().join("cache").exists());
}
#[test]
fn application_check_rejects_plain_model_get_wrapper_without_opening_database() {
let toolchain = TemporaryDirectory::new();
@@ -303,6 +423,68 @@ fn application_check_does_not_apply_production_reachability_to_tests() {
assert!(!toolchain.path().join("cache").exists());
}
#[test]
fn test_filter_still_checks_unselected_sources_and_reports_no_match_before_compilation() {
let toolchain = TemporaryDirectory::new();
let compiler = compiler(toolchain.path(), "dever");
let project_dir = TemporaryDirectory::new();
let root = project_dir.path();
project(root);
write(root, "test/sample/echo/first.dever", "first() () { assert_eq(1, 1) }");
write(root, "test/sample/echo/second.dever", "second() () { assert_eq(1, 1) }");
for filter in ["sam", "sample.ec", "sample.echo.fir", "sample.echo.missing"] {
let result = cli(&compiler, root, "test", &["--filter", filter]);
failure(&result, "no tests match filter");
assert!(!String::from_utf8_lossy(&result.stdout).contains("runtime pack"));
}
let selected = cli(&compiler, root, "test", &["--filter", "sample.echo.second"]);
failure(&selected, "runtime pack");
let report = String::from_utf8_lossy(&selected.stdout);
assert!(report.contains("0 passed; 1 failed; 1 not run"), "{report}");
assert!(!report.contains("test sample/echo/first ..."), "{report}");
write(root, "test/sample/echo/first.dever", "first() () { missing() }");
failure(&cli(&compiler, root, "test", &["--filter", "sample.echo.second"]), "C004");
assert!(!toolchain.path().join("cache").exists());
}
#[test]
#[ignore = "requires an explicitly prepared runtime pack adjacent to CARGO_BIN_EXE_dever; owned short-lived cases only"]
fn llvm_test_controls_preserve_indices_prefix_boundaries_and_timeout_continuation() {
let compiler = Path::new(env!("CARGO_BIN_EXE_dever"));
let directory = TemporaryDirectory::new();
let root = directory.path();
project(root);
for (relative, source) in [
("test/sample/echo/alpha.dever", "alpha() () { assert_eq(1, 2) }"),
("test/sample/echo/beta.dever", "beta() () { assert_eq(7, 7) }"),
("test/sample/echo/slow.dever", "slow() () { dever.task.sleep(30000) }"),
("test/sample/echo/zeta.dever", "zeta() () { assert_eq(9, 9) }"),
("test/sample/echoes/first.dever", "first() () { assert_eq(1, 2) }"),
("test/samples/echo/first.dever", "first() () { assert_eq(1, 2) }"),
] {
write(root, relative, source);
}
let selected = cli(compiler, root, "test", &["--filter", "sample.echo.beta"]);
success(&selected);
let report = String::from_utf8_lossy(&selected.stdout);
assert!(report.contains("test sample/echo/beta ... ok"), "{report}");
assert!(report.contains("1 passed; 0 failed; 5 not run"), "{report}");
assert!(!report.contains("sample/echo/alpha"), "{report}");
for (filter, counts) in [
("sample.echo", "2 passed; 2 failed; 2 not run"),
("sample", "2 passed; 3 failed; 1 not run"),
] {
let selected = cli(compiler, root, "test", &["--filter", filter, "--timeout-ms", "250"]);
failure(&selected, counts);
let report = String::from_utf8_lossy(&selected.stdout);
assert!(report.contains("test process timed out after 250 ms"), "{report}");
assert!(report.contains("test sample/echo/zeta ... ok"), "{report}");
assert!(!report.contains("test samples/echo/first"), "{report}");
}
failure(&cli(compiler, root, "test", &["--filter", "sample.echo.bet"]), "no tests match filter");
}
#[test]
fn cross_build_requires_the_selected_pack_even_when_host_pack_exists() {
let root = TemporaryDirectory::new();
+246
View File
@@ -0,0 +1,246 @@
use super::*;
use std::os::unix::fs::{PermissionsExt, symlink};
use std::process::Output;
struct Project {
temporary: TemporaryDirectory,
layout: Layout,
root: PathBuf,
key: Ed25519KeyPair,
}
impl Project {
fn new() -> Self {
let temporary = TemporaryDirectory::new();
let layout = Layout::new(temporary.path().join("machine"));
layout.initialize().unwrap();
let key = signing_key();
fs::write(
layout.state().join("trusted-release-key"),
format!("{}\n", hex(key.public_key().as_ref())),
)
.unwrap();
write_release(
&layout,
&key,
"1.0.0",
&temporary.path().join("dispatch"),
true,
true,
);
MachineManager::new(layout.clone())
.install_requested("1.0.0")
.unwrap();
fs::copy(env!("CARGO_BIN_EXE_dever-launcher"), layout.bin().join("dever")).unwrap();
let root = temporary.path().join("project");
fs::create_dir_all(root.join("module")).unwrap();
fs::create_dir(root.join("config")).unwrap();
fs::write(root.join("config/setting.json"), "{}").unwrap();
Self {
temporary,
layout,
root,
key,
}
}
fn run(&self, root: &Path, extra: &[&str]) -> Output {
Command::new(self.layout.bin().join("dever"))
.args(["skill", "install"])
.args(extra)
.current_dir(root)
.env_clear()
.output()
.unwrap()
}
fn succeeds(&self) {
let output = self.run(&self.root, &[]);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert!(String::from_utf8_lossy(&output.stdout).contains("AGENTS.md"));
self.assert_no_staging();
}
fn fails(&self, message: &str) {
let output = self.run(&self.root, &[]);
assert!(!output.status.success());
assert!(
String::from_utf8_lossy(&output.stderr).contains(message),
"{}",
String::from_utf8_lossy(&output.stderr)
);
self.assert_no_staging();
}
fn assert_no_staging(&self) {
assert!(fs::read_dir(&self.root).unwrap().all(|entry| {
!entry
.unwrap()
.file_name()
.to_string_lossy()
.starts_with(".dever-skill-")
}));
}
fn sign_manifest(&self, manifest: &ReleaseManifest) {
let release = self.layout.versions().join("1.0.0");
let bytes = serde_json::to_vec(manifest).unwrap();
fs::write(release.join("manifest.json"), &bytes).unwrap();
fs::write(
release.join("manifest.sig"),
hex(self.key.sign(&bytes).as_ref()),
)
.unwrap();
}
}
#[test]
fn project_skill_preserves_user_bytes_permissions_and_updates_only_its_block() {
let fixture = Project::new();
let agents = fixture.root.join("AGENTS.md");
let before = "# 项目规则\r\n保留用户内容,无末尾换行";
fs::write(&agents, before).unwrap();
fs::set_permissions(&agents, fs::Permissions::from_mode(0o640)).unwrap();
fixture.succeeds();
assert_eq!(
fs::read_to_string(&agents).unwrap(),
format!("{before}\n\n{}", project_rules("1.0.0"))
);
assert_eq!(
fs::metadata(&agents).unwrap().permissions().mode() & 0o777,
0o640
);
let installed = fs::read(&agents).unwrap();
fixture.succeeds();
assert_eq!(fs::read(&agents).unwrap(), installed);
let prefix = "# 自定义前文\r\n\r\n";
let suffix = "\r\n# 自定义后文\r\n原样保留";
fs::write(
&agents,
format!("{prefix}<!-- dever-language:start -->\n旧规则\n<!-- dever-language:end -->{suffix}"),
)
.unwrap();
fixture.succeeds();
assert_eq!(
fs::read_to_string(&agents).unwrap(),
format!("{prefix}{}{suffix}", project_rules("1.0.0").trim_end())
);
}
#[test]
fn project_skill_rejects_ambiguous_or_unsafe_agents_without_changes() {
let fixture = Project::new();
let agents = fixture.root.join("AGENTS.md");
for content in [
"<!-- dever-language:start -->",
"<!-- dever-language:end -->",
"<!-- dever-language:end --><!-- dever-language:start -->",
"<!-- dever-language:start --><!-- dever-language:start --><!-- dever-language:end -->",
"<!-- dever-language:start --><!-- dever-language:end --><!-- dever-language:start --><!-- dever-language:end -->",
] {
fs::write(&agents, content).unwrap();
fixture.fails("exactly one complete");
assert_eq!(fs::read_to_string(&agents).unwrap(), content);
}
fs::write(&agents, [0xff, 0xfe]).unwrap();
fixture.fails("UTF-8");
assert_eq!(fs::read(&agents).unwrap(), [0xff, 0xfe]);
fs::write(&agents, "do not overwrite").unwrap();
fs::set_permissions(&agents, fs::Permissions::from_mode(0o444)).unwrap();
fixture.fails("read-only");
fs::set_permissions(&agents, fs::Permissions::from_mode(0o644)).unwrap();
fs::remove_file(&agents).unwrap();
fs::create_dir(&agents).unwrap();
fixture.fails("regular file");
fs::remove_dir(&agents).unwrap();
let other = fixture.temporary.path().join("other-agents");
fs::write(&other, "untouched").unwrap();
symlink(&other, &agents).unwrap();
fixture.fails("regular file");
assert!(fs::symlink_metadata(&agents).unwrap().is_symlink());
assert_eq!(fs::read_to_string(&other).unwrap(), "untouched");
}
#[test]
fn project_skill_requires_project_root_valid_version_and_signed_template() {
let fixture = Project::new();
let agents = fixture.root.join("AGENTS.md");
let output = fixture.run(&fixture.root, &["unwanted-directory"]);
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("accepts no arguments"));
assert!(!fixture.root.join("unwanted-directory").exists());
let output = fixture.run(fixture.temporary.path(), &[]);
assert!(!output.status.success());
assert!(!fixture.temporary.path().join("AGENTS.md").exists());
fs::remove_dir(fixture.root.join("module")).unwrap();
symlink(fixture.temporary.path(), fixture.root.join("module")).unwrap();
fixture.fails("real module/");
fs::remove_file(fixture.root.join("module")).unwrap();
fs::create_dir(fixture.root.join("module")).unwrap();
let settings = fixture.root.join("config/setting.json");
for invalid in [
"not json",
"[]",
r#"{"dever":{"version":"latest"}}"#,
r#"{"dever":{"version":"9.0.0"}}"#,
] {
fs::write(&settings, invalid).unwrap();
let output = fixture.run(&fixture.root, &[]);
assert!(!output.status.success());
assert!(!agents.exists());
}
fs::remove_file(&settings).unwrap();
fixture.fails("cannot read");
fs::write(&settings, "{}").unwrap();
let template = fixture
.layout
.versions()
.join("1.0.0/skills/dever-language/assets/template/AGENTS.md");
fs::write(&template, "tampered instructions").unwrap();
let output = fixture.run(&fixture.root, &[]);
assert!(!output.status.success());
assert!(!agents.exists());
fixture.assert_no_staging();
}
#[test]
fn project_skill_refuses_unsigned_extra_or_invalid_signed_template() {
let fixture = Project::new();
let agents = fixture.root.join("AGENTS.md");
fs::write(&agents, "user-owned instructions").unwrap();
let release = fixture.layout.versions().join("1.0.0");
let mut manifest: ReleaseManifest =
serde_json::from_slice(&fs::read(release.join("manifest.json")).unwrap()).unwrap();
let logical = "skills/dever-language/assets/template/AGENTS.md";
manifest.artifacts.retain(|entry| entry.path != logical);
fixture.sign_manifest(&manifest);
fixture.fails("does not declare resource");
assert_eq!(
fs::read_to_string(&agents).unwrap(),
"user-owned instructions"
);
let template = release.join(logical);
for invalid in [
"no managed block",
"<!-- dever-language:start -->missing end",
"outside\n<!-- dever-language:start -->rules<!-- dever-language:end -->",
] {
fs::write(&template, invalid).unwrap();
manifest.artifacts.retain(|entry| entry.path != logical);
manifest.artifacts.push(artifact(&template, logical));
fixture.sign_manifest(&manifest);
let output = fixture.run(&fixture.root, &[]);
assert!(!output.status.success());
assert_eq!(
fs::read_to_string(&agents).unwrap(),
"user-owned instructions"
);
fixture.assert_no_staging();
}
}
+53 -25
View File
@@ -22,6 +22,10 @@ static NEXT_TEMPORARY: AtomicU64 = AtomicU64::new(0);
#[path = "shared_toolchain_extensions.rs"]
mod extensions;
#[cfg(target_os = "linux")]
#[path = "project_skill.rs"]
mod project_skill;
struct TemporaryDirectory(PathBuf);
impl TemporaryDirectory {
@@ -306,30 +310,16 @@ fn signed_versions_install_select_dispatch_update_and_rollback_atomically() {
fs::read_to_string(skill_path(&layout).join("SKILL.md")).unwrap(),
"skill 1.0.0\n"
);
let skill_entry = temporary.path().join("dever-language");
execute_in(
&layout,
&[
"skill".into(),
"install".into(),
skill_entry.as_os_str().into(),
],
)
.unwrap();
let loader = fs::read(skill_entry.join("SKILL.md")).unwrap();
assert!(String::from_utf8_lossy(&loader).contains("skill path"));
assert!(
execute_in(
&layout,
&[
"skill".into(),
"install".into(),
skill_entry.as_os_str().into()
]
)
.is_err()
);
assert_eq!(fs::read(skill_entry.join("SKILL.md")).unwrap(), loader);
fs::copy(env!("CARGO_BIN_EXE_dever-launcher"), layout.bin().join("dever")).unwrap();
let active_project = temporary.path().join("active-project");
fs::create_dir_all(active_project.join("module")).unwrap();
fs::create_dir(active_project.join("config")).unwrap();
fs::write(active_project.join("config/setting.json"), "{}").unwrap();
install_project_skill(&layout, &active_project);
let rules = fs::read(active_project.join("AGENTS.md")).unwrap();
assert_eq!(rules, project_rules("1.0.0").as_bytes());
install_project_skill(&layout, &active_project);
assert_eq!(fs::read(active_project.join("AGENTS.md")).unwrap(), rules);
let new_project = temporary.path().join("not-created-yet");
let CommandResult::Exited(code) = execute_in(
@@ -351,6 +341,7 @@ fn signed_versions_install_select_dispatch_update_and_rollback_atomically() {
let project = temporary.path().join("project");
fs::create_dir_all(project.join("config")).unwrap();
fs::create_dir(project.join("module")).unwrap();
fs::write(
project.join("config/setting.json"),
r#"{"dever":{"version":"2.0.0"}}"#,
@@ -368,6 +359,11 @@ fn signed_versions_install_select_dispatch_update_and_rollback_atomically() {
Path::new(&pinned_skill),
layout.versions().join("2.0.0/skills/dever-language")
);
install_project_skill(&layout, &project);
assert_eq!(
fs::read_to_string(project.join("AGENTS.md")).unwrap(),
project_rules("2.0.0")
);
let CommandResult::Exited(code) = execute_in(&layout, &arguments).unwrap() else {
panic!("project command must dispatch")
};
@@ -393,7 +389,12 @@ fn signed_versions_install_select_dispatch_update_and_rollback_atomically() {
fs::read_to_string(skill_path(&layout).join("SKILL.md")).unwrap(),
"skill 2.0.0\n"
);
assert_eq!(fs::read(skill_entry.join("SKILL.md")).unwrap(), loader);
assert_eq!(fs::read(active_project.join("AGENTS.md")).unwrap(), rules);
install_project_skill(&layout, &active_project);
assert_eq!(
fs::read_to_string(active_project.join("AGENTS.md")).unwrap(),
project_rules("2.0.0")
);
fs::write(layout.downloads().join("latest"), "1.0.0\n").unwrap();
assert!(
manager
@@ -1342,12 +1343,16 @@ fn write_release(
let skill = root.join("skills/dever-language/SKILL.md");
fs::create_dir_all(skill.parent().unwrap()).unwrap();
fs::write(&skill, format!("skill {version}\n")).unwrap();
let rules = root.join("skills/dever-language/assets/template/AGENTS.md");
fs::create_dir_all(rules.parent().unwrap()).unwrap();
fs::write(&rules, project_rules(version)).unwrap();
let manifest = ReleaseManifest::new(
Version::parse(version).unwrap(),
vec![
artifact(&core, "dever-core"),
artifact(&runtime, "runtime.pack"),
artifact(&skill, "skills/dever-language/SKILL.md"),
artifact(&rules, "skills/dever-language/assets/template/AGENTS.md"),
],
);
let bytes = serde_json::to_vec(&manifest).unwrap();
@@ -1359,6 +1364,29 @@ fn write_release(
fs::write(root.join("manifest.sig"), format!("{}\n", hex(&signature))).unwrap();
}
#[cfg(unix)]
fn project_rules(version: &str) -> String {
include_str!("../../skills/assets/template/AGENTS.md").replace(
"<!-- dever-language:end -->",
&format!("<!-- fixture version {version} -->\n<!-- dever-language:end -->"),
)
}
#[cfg(unix)]
fn install_project_skill(layout: &Layout, project: &Path) {
let output = Command::new(layout.bin().join("dever"))
.args(["skill", "install"])
.current_dir(project)
.env_clear()
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
}
fn artifact(path: &Path, relative: &str) -> Artifact {
let bytes = fs::read(path).unwrap();
Artifact {
+84
View File
@@ -0,0 +1,84 @@
use super::{Action, OsString, arguments_for};
use std::time::Duration;
#[test]
fn test_options_default_to_sixty_seconds_and_accept_either_order() {
for options in [
vec![],
vec!["--filter", "sample.account.case_2"],
vec!["--filter", "sample.account", "--timeout-ms", "15"],
vec!["--timeout-ms", "15", "--filter", "sample"],
vec!["--timeout-ms", "18446744073709551615"],
] {
let arguments: Vec<_> = [vec!["test", "project"], options.clone()]
.concat()
.into_iter()
.map(OsString::from)
.collect();
let (root, action) = arguments_for(&arguments).unwrap();
assert_eq!(root.to_str(), Some("project"));
let Action::Test(parsed) = action else {
panic!("expected test")
};
let timeout = options
.iter()
.position(|option| *option == "--timeout-ms")
.map(|index| options[index + 1].parse::<u64>().unwrap())
.unwrap_or(60_000);
assert_eq!(parsed.timeout, Duration::from_millis(timeout));
assert_eq!(
parsed.filter.as_deref(),
options
.iter()
.position(|option| *option == "--filter")
.map(|index| options[index + 1]),
);
}
}
#[test]
fn test_options_reject_invalid_missing_duplicate_and_unknown_arguments() {
let mut invalid = vec![
vec!["--filter"],
vec!["--timeout-ms"],
vec!["--filter", "--timeout-ms", "20"],
vec!["--filter", "sample", "--filter", "sample.account"],
vec!["--timeout-ms", "20", "--timeout-ms", "30"],
vec!["--unknown", "value"],
vec!["extra"],
];
for filter in [
"", ".sample", "sample.", "sample..case", "sample.a.b.c", "sam*", "sample/account",
"Sample", "sample.__case", "sample.case_", "sample.2case", " sample", "用户",
] {
invalid.push(vec!["--filter", filter]);
}
for timeout in [
"", "0", "-1", "+1", "1.5", "1ms", " 1", "18446744073709551616",
] {
invalid.push(vec!["--timeout-ms", timeout]);
}
for options in invalid {
let arguments: Vec<_> = [vec!["test", "project"], options]
.concat()
.into_iter()
.map(OsString::from)
.collect();
assert!(arguments_for(&arguments).is_err(), "accepted {arguments:?}");
}
}
#[cfg(unix)]
#[test]
fn test_options_reject_non_utf8_values() {
use std::os::unix::ffi::OsStringExt;
for option in ["--filter", "--timeout-ms"] {
let arguments = [
OsString::from("test"),
OsString::from("project"),
OsString::from(option),
OsString::from_vec(vec![0xff]),
];
assert!(arguments_for(&arguments).is_err());
}
}
+114
View File
@@ -0,0 +1,114 @@
use super::{TemporaryProject, process};
use std::fs;
use std::process::Command;
use std::time::{Duration, Instant};
const DESCENDANTS: &str = r#"
/bin/sh -c 'echo $$ > descendant; /bin/sleep 30 & echo $! > grandchild; wait' &
while [ ! -s grandchild ]; do :; done
"#;
// subreaper 是进程级状态;此单例测试顺序覆盖所有真实进程场景。
#[test]
fn case_process_captures_output_times_out_and_reaps_only_owned_descendants() {
let original_reaper = rustix::process::child_subreaper().unwrap();
let mut unrelated = Command::new("/bin/sleep").arg("30").spawn().unwrap();
let result = std::panic::catch_unwind(|| {
for (body, descendants, expected) in [
("printf hidden; printf hidden-error >&2; exit 0", false, None),
(
"printf output; printf diagnostic >&2; exit 7",
false,
Some("exit status: 7"),
),
(
"printf before-timeout; /bin/sleep 30",
false,
Some("timed out after 100 ms"),
),
("exit 0", true, None),
("printf owned-error >&2; exit 7", true, Some("exit status: 7")),
("printf started; wait", true, Some("timed out after 100 ms")),
] {
let root = TemporaryProject::new().unwrap();
let started = Instant::now();
let script = if descendants {
format!("{DESCENDANTS}\n{body}")
} else {
body.to_owned()
};
let result = process::run(
Command::new("/bin/sh")
.arg("-c")
.arg(&script)
.env_clear()
.current_dir(root.path()),
root.path(),
Duration::from_millis(100),
);
assert!(started.elapsed() < Duration::from_secs(5));
match expected {
None => result.unwrap(),
Some(message) => {
let failure = result.unwrap_err();
assert!(failure.message.contains(message), "{failure:?}");
if script.contains("printf output") {
assert_eq!(failure.stdout, b"output");
assert_eq!(failure.stderr, b"diagnostic");
}
if script.contains("printf before-timeout") {
assert_eq!(failure.stdout, b"before-timeout");
}
if script.contains("printf owned-error") {
assert_eq!(failure.stderr, b"owned-error");
}
}
}
for name in ["descendant", "grandchild"] {
let path = root.path().join(name);
if descendants {
let pid = fs::read_to_string(path)
.unwrap()
.trim()
.parse::<u32>()
.unwrap();
assert!(
!std::path::Path::new(&format!("/proc/{pid}")).exists(),
"unreaped {name} {pid}"
);
}
}
assert_eq!(rustix::process::child_subreaper().unwrap(), original_reaper);
}
let root = TemporaryProject::new().unwrap();
let failure = process::run(
Command::new("/bin/sh")
.arg("-c")
.arg("/bin/dd if=/dev/zero bs=131072 count=2 2>/dev/null; printf end >&2; exit 9")
.env_clear(),
root.path(),
Duration::from_secs(5),
)
.unwrap_err();
assert!(failure.message.contains("exit status: 9"));
assert_eq!(failure.stdout.len(), 262_144);
assert_eq!(failure.stderr, b"end");
let root = TemporaryProject::new().unwrap();
let failure = process::run(
&mut Command::new(root.path().join("missing-executable")),
root.path(),
Duration::from_millis(100),
)
.unwrap_err();
assert!(failure.message.contains("cannot run test process"));
assert_eq!(rustix::process::child_subreaper().unwrap(), original_reaper);
});
// 即使断言失败也回收此对照进程,不能让测试自身遗留资源。
let unrelated_running = unrelated.try_wait().unwrap().is_none();
let _ = unrelated.kill();
unrelated.wait().unwrap();
result.unwrap();
assert!(unrelated_running, "case cleanup affected an unrelated child");
}
+4
View File
@@ -86,6 +86,10 @@ path = "tests/markdown_source.rs"
name = "cms_acceptance"
path = "tests/cms_acceptance.rs"
[[test]]
name = "skill_examples"
path = "tests/skill_examples.rs"
[[test]]
name = "native_cache"
path = "tests/native_cache.rs"
@@ -2,6 +2,16 @@ mod support;
use support::{checked, sources};
fn application(files: &[(&str, &str)]) -> dever_core::hir::Program {
let root = support::temp::TemporaryDirectory::new();
for (relative, source) in files {
let path = root.path().join("module").join(relative);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, source).unwrap();
}
checked(&dever_core::source::SourceMap::load(&root.path().join("module")).unwrap())
}
fn warnings(source: &str, code: &str) -> Vec<dever_core::diagnostic::Diagnostic> {
let sources = sources(source);
let user_source = sources.files()[0].id();
@@ -99,3 +109,122 @@ fn existing_unused_write_warning_is_not_duplicated_as_dead_computation() {
.any(|warning| warning.code == "W006")
);
}
#[test]
fn application_rejects_reachable_private_copies_with_renamed_inputs() {
let program = application(&[
("sample/count/domain.dever", "first(value: Int) (answer: Int) pure { answer = value + 1 }\nsecond(renamed: Int) (answer: Int) pure { answer = renamed + 1 }"),
("sample/count/app.dever", "total(value: Int) (answer: Int) { first = domain.first(value)\nsecond = domain.second(value)\nanswer = first + second }"),
("sample/count/api.dever", "cmd total = app.total"),
]);
let warnings: Vec<_> = program.warnings().iter().filter(|item| item.code == "W003").collect();
let errors = program.application_errors().unwrap();
assert_eq!(errors.len(), 1, "{errors:?}");
assert_eq!(errors[0].code, "C016");
assert!(errors[0].message.contains("same implementation and contract"));
assert!(errors[0].message.contains("domain.first"));
assert!(errors[0].message.contains("domain.second"));
assert_eq!(errors[0].related.len(), 1);
assert!(warnings.iter().any(|warning| warning.primary == errors[0].primary));
assert_ne!(errors[0].primary, errors[0].related[0].span);
}
#[test]
fn application_rejects_repeated_pure_calls_using_the_warning_proof() {
let program = application(&[
("sample/count/domain.dever", "calculate(value: Int) (answer: Int) pure { answer = value + 1 }"),
("sample/count/app.dever", "total(value: Int) (answer: Int) { first = domain.calculate(value)\nsecond = domain.calculate(value)\nanswer = first + second }"),
("sample/count/api.dever", "cmd total = app.total"),
]);
let errors = program.application_errors().unwrap();
assert_eq!(errors.len(), 1, "{errors:?}");
assert_eq!(errors[0].code, "C016");
assert!(errors[0].message.contains("earlier result is still available"));
assert_eq!(errors[0].related.len(), 1);
assert!(program.warnings().iter().any(|warning| {
warning.code == "W004" && warning.primary == errors[0].primary
}));
}
#[test]
fn application_does_not_reject_changed_inputs_results_or_effectful_calls() {
for (app, domain, api) in [
(
"total(value: Int) (answer: Int) { first = domain.calculate(value)\nvalue = value + 1\nsecond = domain.calculate(value)\nanswer = first + second }",
"calculate(value: Int) (answer: Int) pure { answer = value + 1 }",
"cmd total = app.total",
),
(
"total(value: Int) (answer: Int) { first = domain.calculate(value)\nfirst = 0\nsecond = domain.calculate(value)\nanswer = first + second }",
"calculate(value: Int) (answer: Int) pure { answer = value + 1 }",
"cmd total = app.total",
),
(
"type Counter { value: Int }\ntotal(counter: Counter) (answer: Int) { first = domain.calculate(counter.value)\ncounter.value = counter.value + 1\nsecond = domain.calculate(counter.value)\nanswer = first + second }",
"calculate(value: Int) (answer: Int) pure { answer = value + 1 }",
"cmd total = app.total",
),
(
"noisy(value: Int) (answer: Int) { dever.io.println(\"called\")\nanswer = value }\ntotal(value: Int) (answer: Int) { first = noisy(value)\nsecond = noisy(value)\nanswer = first + second }",
"",
"cmd total = app.total",
),
] {
let program = application(&[
("sample/count/app.dever", app),
("sample/count/domain.dever", domain),
("sample/count/api.dever", api),
]);
let errors = program.application_errors().unwrap();
assert!(errors.is_empty(), "{app}: {errors:?}");
}
}
#[test]
fn application_preserves_different_contracts_and_owners() {
for domain in [
"first(value: Int) (answer: Int) pure { answer = value }\nsecond(value: Int) (different: Int) pure { different = value }",
"first(value: Int) (answer: Int) pure recover(\"first policy\") { answer = value }\nsecond(value: Int) (answer: Int) pure recover(\"second policy\") { answer = value }",
"first() (answer: Int >= 1) pure { answer = 1 }\nsecond() (answer: Int >= 0) pure { answer = 1 }",
] {
let inputs = if domain.contains("value:") { "1" } else { "" };
let app = format!("total() (answer: Int) {{ first = domain.first({inputs})\nsecond = domain.second({inputs})\nanswer = first + second }}");
let program = application(&[
("sample/count/domain.dever", domain),
("sample/count/app.dever", &app),
("sample/count/api.dever", "cmd total = app.total"),
]);
assert!(program.application_errors().unwrap().is_empty(), "{domain}");
}
let program = application(&[
("first/count/domain.dever", "calculate(value: Int) (answer: Int) pure { answer = value + 1 }"),
("first/count/app.dever", "total(value: Int) (answer: Int) { answer = domain.calculate(value) }"),
("first/count/api.dever", "cmd total = app.total"),
("second/count/domain.dever", "calculate(value: Int) (answer: Int) pure { answer = value + 1 }"),
("second/count/app.dever", "total(value: Int) (answer: Int) { answer = domain.calculate(value) }"),
("second/count/api.dever", "cmd total = app.total"),
]);
assert!(program.application_errors().unwrap().is_empty());
}
#[test]
fn application_keeps_effectful_private_copies_and_adapter_redundancy_as_warnings() {
let program = application(&[
("sample/count/domain.dever", "emit(value: Int) (answer: Int) { dever.io.println(\"called\")\nanswer = value }\nfirst(value: Int) (answer: Int) { answer = emit(value) + 1 }\nsecond(renamed: Int) (answer: Int) { answer = emit(renamed) + 1 }"),
("sample/count/app.dever", "total(value: Int) (answer: Int) { first = domain.first(value)\nsecond = domain.second(value)\nanswer = first + second }"),
("sample/count/api.dever", "cmd total = app.total"),
]);
assert!(program.warnings().iter().any(|warning| warning.code == "W003"));
assert!(program.application_errors().unwrap().is_empty());
let program = application(&[
("sample/count/port.dever", "read(value: Int) (answer: Int) fails dever.time.SleepResult"),
("sample/count/adapter.dever", "first(value: Int) (answer: Int) { answer = value + 1 }\nsecond(value: Int) (answer: Int) { answer = value + 1 }\nport.read(value: Int) (answer: Int) { one = first(value)\ntwo = second(value)\nthree = first(value)\nanswer = one + two + three }"),
("sample/count/app.dever", "read(value: Int) (answer: Int) { answer = port.read(value) }"),
("sample/count/api.dever", "cmd read = app.read"),
]);
for code in ["W003", "W004"] {
assert!(program.warnings().iter().any(|warning| warning.code == code));
}
assert!(program.application_errors().unwrap().is_empty());
}
+132
View File
@@ -0,0 +1,132 @@
use std::fs;
use std::path::{Path, PathBuf};
use dever_core::{hir::Program, source::SourceMap};
#[path = "support/temp.rs"]
mod temp;
fn examples() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../skills/assets/examples")
}
fn checked(root: &Path) -> Program {
let sources = SourceMap::load_project(&root.join("module"), &root.join("test")).unwrap();
let settings = dever_runtime::config::Settings::load_project(root).unwrap();
let program = dever_core::check_with_settings(&sources, &settings).unwrap_or_else(|errors| {
panic!(
"{}: {}",
root.display(),
errors
.iter()
.map(|error| error.render(&sources))
.collect::<String>()
)
});
let errors = program.application_errors().unwrap();
assert!(
errors.is_empty(),
"{}",
errors
.iter()
.map(|error| error.render(&sources))
.collect::<String>()
);
program.validate_database_settings(&settings).unwrap();
for source in sources.files() {
assert_eq!(
dever_core::format::format(source).unwrap(),
source.text(),
"{}",
source.path().display()
);
}
program
}
fn copy_tree(source: &Path, target: &Path) {
fs::create_dir(target).unwrap();
for entry in fs::read_dir(source).unwrap() {
let entry = entry.unwrap();
let destination = target.join(entry.file_name());
if entry.file_type().unwrap().is_dir() {
copy_tree(&entry.path(), &destination);
} else {
fs::copy(entry.path(), destination).unwrap();
}
}
}
#[test]
fn relocated_examples_check_production_roots_settings_and_case_local_effects() {
let temporary = temp::TemporaryDirectory::new();
let projects = [
("http-rest", 1, false),
("jobs", 1, true),
("external-lib", 2, false),
("collections", 2, false),
("command", 1, false),
];
assert_eq!(fs::read_dir(examples()).unwrap().count(), projects.len());
for (name, test_count, uses_database) in projects {
let root = temporary.path().join(name);
copy_tree(&examples().join(name), &root);
let program = checked(&root);
assert_eq!(program.tests().len(), test_count);
assert!(
program
.tests()
.iter()
.all(|case| case.uses_database() == uses_database)
);
}
let template = temporary.path().join("template");
copy_tree(&examples().parent().unwrap().join("template"), &template);
assert_eq!(checked(&template).tests().len(), 1);
}
#[test]
fn three_ecosystems_share_one_checked_port_contract_and_fake_suite() {
let temporary = temp::TemporaryDirectory::new();
let root = temporary.path().join("external-lib");
copy_tree(&examples().join("external-lib"), &root);
let original = checked(&root).external_worker_contracts().remove(0);
let mut schemas = std::collections::BTreeSet::new();
for (ecosystem, entry, dependency) in [
("pip", "python/render.py", "markdown@3.8.2"),
("npm", "javascript/render.mjs", "marked@15.0.12"),
("go", "go/render.go", "github.com/yuin/goldmark@v1.7.8"),
] {
fs::write(
root.join("module/document/markup/adapter.dever"),
format!("external {ecosystem} \"{entry}\" {{\n lib \"{dependency}\"\n}}\n"),
)
.unwrap();
fs::write(
root.join("config/setting.json"),
serde_json::to_vec(&serde_json::json!({
"lib": [format!("{ecosystem}:{dependency}")]
}))
.unwrap(),
)
.unwrap();
let program = checked(&root);
let contracts = program.external_worker_contracts();
let [contract] = contracts.as_slice() else {
panic!("one external Adapter must own the shared Port")
};
// 协议身份绑定生态和 Lib 声明;跨生态复用的是 Port 的类型合同。
assert!(schemas.insert(contract.schema.clone()));
assert_eq!(contract.port, original.port);
assert_eq!(contract.setting, original.setting);
assert_eq!(contract.inputs, original.inputs);
assert_eq!(contract.outputs, original.outputs);
assert_eq!(contract.errors, original.errors);
assert_eq!(contract.operations, ["render"]);
assert_eq!(contract.ecosystem, ecosystem);
assert_eq!(contract.libs, [format!("{ecosystem}:{dependency}")]);
assert!(root.join(&contract.entry).is_file());
assert_eq!(program.tests().len(), 2);
assert!(!program.tests().iter().any(|case| case.uses_database()));
}
}
+18 -6
View File
@@ -128,7 +128,7 @@ class InstalledDaemon:
self.__exit__(None, None, None)
raise
def command(self, *arguments):
def command(self, *arguments, cwd="/"):
if self.child.poll() is not None:
raise RuntimeError("owned daemon exited")
expected = self.image.stat()
@@ -137,7 +137,7 @@ class InstalledDaemon:
raise RuntimeError("owned namespace identity changed")
return execute([
"/usr/bin/nsenter", "--target", str(self.pid), "--all", "--root",
"--wdns=/", "--", "/usr/local/bin/dever", *arguments,
f"--wdns={cwd}", "--", "/usr/local/bin/dever", *arguments,
])
def __exit__(self, *_):
@@ -277,10 +277,22 @@ def accept(release, assets, image, report, init, extension_helper=None):
expected = f"/opt/dever/versions/{manifest['version']}/skills/dever-language"
if daemon.command("skill", "path") != expected:
raise RuntimeError("versioned skill did not match installed core")
daemon.command("skill", "install", "/ai-skill")
if not (image / "ai-skill/SKILL.md").is_file():
raise RuntimeError("missing installed skill entry")
passed("versioned_skill_resolution_and_loader_install")
rules = image / "first/AGENTS.md"
project_rules = b"# Project-specific rules\n\n"
rules.write_bytes(project_rules + (
"<!-- dever-language:start -->\n"
"Previous Dever rules\n"
"<!-- dever-language:end -->\n"
).encode())
daemon.command("skill", "install", cwd="/first")
template = installed / "skills/dever-language/assets/template/AGENTS.md"
expected_rules = project_rules + template.read_bytes()
if rules.read_bytes() != expected_rules:
raise RuntimeError("project rules differ from the signed template or user rules were lost")
daemon.command("skill", "install", cwd="/first")
if rules.read_bytes() != expected_rules:
raise RuntimeError("repeated skill installation changed project rules")
passed("versioned_skill_resolution_and_project_rules_install")
try:
daemon.command("build", "/first", "--target", "linux-aarch64", "--output", "/first/program.arm64")
except RuntimeError as error:
+125
View File
@@ -0,0 +1,125 @@
"""Focused static checks for the self-contained, versioned language skill."""
import re
import shutil
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SKILL = ROOT / "skills"
def check_tutorial_sources(skill):
"""Keep tutorial excerpts tied to the complete projects checked by Rust."""
projects = [
path.resolve()
for path in [skill / "assets/template", *(skill / "assets/examples").iterdir()]
]
for document in sorted((skill / "references").glob("*.md")):
content = document.read_text()
blocks = list(re.finditer(
r"^```dever[ \t]*\n(.*?)^```[ \t]*$", content, re.MULTILINE | re.DOTALL
))
if len(blocks) != len(re.findall(r"^```dever\b", content, re.MULTILINE)):
raise ValueError(f"{document.name}: unclosed or unsupported dever fence")
for block in blocks:
binding = re.search(
r"源码:\[[^\]\n]+\]\(([^)\n]+)\)\n\n\Z", content[:block.start()]
)
if binding is None:
raise ValueError(f"{document.name}: dever block needs a source link")
source = (document.parent / binding.group(1)).resolve()
owners = [root for root in projects if source.is_relative_to(root)]
if len(owners) != 1 or source.suffix != ".dever":
raise ValueError(f"{document.name}: source must belong to a checked skill project")
relative = source.relative_to(owners[0])
if relative.parts[0] not in ("module", "test") or not source.is_file():
raise ValueError(f"{document.name}: source must be a module or test file")
excerpt = block.group(1)
if not excerpt.strip() or ("\n" + source.read_text()).count("\n" + excerpt) != 1:
raise ValueError(f"{document.name}: excerpt differs from its source or is ambiguous")
class SkillExamples(unittest.TestCase):
def test_relocated_skill_links_and_release_file_limits(self):
with tempfile.TemporaryDirectory(prefix="dever-skill-") as directory:
installed = Path(directory) / "dever-language"
shutil.copytree(SKILL, installed)
files = [path for path in installed.rglob("*") if path.is_file()]
self.assertLessEqual(len(files), 256)
self.assertLessEqual(sum(path.stat().st_size for path in files), 8 * 1024 * 1024)
self.assertTrue((installed / "SKILL.md").is_file())
for path in files:
self.assertFalse(path.is_symlink())
self.assertLessEqual(path.stat().st_size, 1024 * 1024)
if path.suffix != ".md":
continue
for target in re.findall(r"\]\(([^)]+)\)", path.read_text()):
if "://" in target or target.startswith("#"):
continue
resolved = (path.parent / target.split("#", 1)[0]).resolve()
self.assertTrue(resolved.is_relative_to(installed), (path, target))
self.assertTrue(resolved.exists(), (path, target))
def test_tutorial_code_matches_relocated_checked_sources(self):
with tempfile.TemporaryDirectory(prefix="dever-tutorial-") as directory:
installed = Path(directory) / "dever-language"
shutil.copytree(SKILL, installed)
check_tutorial_sources(installed)
def test_tutorial_source_drift_and_unbound_code_are_rejected(self):
with tempfile.TemporaryDirectory(prefix="dever-tutorial-drift-") as directory:
installed = Path(directory) / "dever-language"
shutil.copytree(SKILL, installed)
document = installed / "references/language.md"
original = document.read_text()
source = installed / "assets/template/module/hello/greeting/app.dever"
original_source = source.read_text()
mutations = {
"unbound": original.replace("源码:", "完整示例:", 1),
"changed_code": original.replace('"Hello, "', '"Welcome, "', 1),
"unclosed": original + "\n```dever\nunclosed() () {}\n",
"outside_project": original.replace(
"../assets/template/module/hello/greeting/app.dever", "../SKILL.md", 1
),
}
for name, changed in mutations.items():
with self.subTest(name=name):
document.write_text(changed)
with self.assertRaises(ValueError):
check_tutorial_sources(installed)
document.write_text(original)
source.write_text(original_source.replace('"Hello, "', '"Welcome, "'))
with self.assertRaisesRegex(ValueError, "excerpt differs"):
check_tutorial_sources(installed)
source.write_text(original_source + "\n" + original_source)
with self.assertRaisesRegex(ValueError, "ambiguous"):
check_tutorial_sources(installed)
def test_signature_index_matches_public_library_declarations(self):
index = (SKILL / "references/signatures.md").read_text()
blocks = re.findall(r"```signatures\n(.*?)```", index, re.DOTALL)
self.assertTrue(blocks, "the shipped index must contain concrete signatures")
seen = set()
for signature in "\n".join(blocks).splitlines():
if not signature:
continue
qualified = signature.split("(", 1)[0]
package, function = qualified.rsplit(".", 1)
library = ROOT / "library" / (package.replace(".", "/") + ".dever")
self.assertNotIn(qualified, seen)
seen.add(qualified)
declaration = re.search(
rf"^public ({re.escape(function)}\s*\([^{{]*?)\s*\{{",
library.read_text(),
re.MULTILINE,
)
self.assertIsNotNone(declaration, qualified)
expected = package + "." + declaration.group(1)
self.assertEqual(re.sub(r"\s", "", signature), re.sub(r"\s", "", expected), qualified)
if __name__ == "__main__":
unittest.main()