@mison/ling 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/rules/GEMINI.md +17 -0
- package/.agents/skills/clean-code/SKILL.md +24 -14
- package/.agents/skills/doc.md +9 -5
- package/CHANGELOG.md +34 -1
- package/README.md +138 -195
- package/bin/adapters/gemini.js +6 -2
- package/bin/core/generator.js +1 -0
- package/bin/interactive.js +6 -4
- package/bin/ling-cli.js +893 -170
- package/bin/utils.js +52 -9
- package/docs/PLAN.md +21 -17
- package/docs/TECH.md +40 -13
- package/package.json +1 -1
- package/scripts/ci-verify.js +4 -1
- package/scripts/health-check.js +4 -13
- package/tests/cli-smoke.test.js +115 -0
- package/tests/global-sync.test.js +15 -2
- package/tests/spec-init-doctor.test.js +233 -0
- package/tests/spec-profile.test.js +84 -0
- package/tests/standards-compliance.test.js +31 -0
- package/.agents/skills/vulnerability-scanner/scripts/__pycache__/security_scan.cpython-310.pyc +0 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
const { test, describe, beforeEach, afterEach } = require("node:test");
|
|
2
|
+
const assert = require("node:assert");
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { spawnSync } = require("node:child_process");
|
|
7
|
+
|
|
8
|
+
const REPO_ROOT = path.resolve(__dirname, "..");
|
|
9
|
+
const CLI_PATH = path.join(REPO_ROOT, "bin", "ling.js");
|
|
10
|
+
|
|
11
|
+
function runCli(args, options = {}) {
|
|
12
|
+
const env = {
|
|
13
|
+
...process.env,
|
|
14
|
+
LING_SKIP_UPSTREAM_CHECK: "1",
|
|
15
|
+
...options.env,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
return spawnSync(process.execPath, [CLI_PATH, ...args], {
|
|
19
|
+
cwd: options.cwd || REPO_ROOT,
|
|
20
|
+
env,
|
|
21
|
+
encoding: "utf8",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("Spec init/doctor", () => {
|
|
26
|
+
let tempRoot;
|
|
27
|
+
let workspaceRoot;
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ling-spec-init-"));
|
|
31
|
+
workspaceRoot = path.join(tempRoot, "workspace");
|
|
32
|
+
fs.mkdirSync(workspaceRoot, { recursive: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("spec init should create workspace assets and doctor should report installed", () => {
|
|
40
|
+
const env = {
|
|
41
|
+
LING_GLOBAL_ROOT: tempRoot,
|
|
42
|
+
LING_INDEX_PATH: path.join(tempRoot, "workspaces.json"),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const initResult = runCli(["spec", "init", "--path", workspaceRoot, "--quiet"], { env });
|
|
46
|
+
assert.strictEqual(initResult.status, 0, initResult.stderr || initResult.stdout);
|
|
47
|
+
|
|
48
|
+
assert.ok(fs.existsSync(path.join(workspaceRoot, "issues.csv")), "issues.csv should be created");
|
|
49
|
+
assert.ok(fs.existsSync(path.join(workspaceRoot, ".ling", "spec", "templates", "driver-prompt.md")), "spec templates should be created");
|
|
50
|
+
assert.ok(fs.existsSync(path.join(workspaceRoot, ".ling", "spec", "references", "gda-framework.md")), "spec references should be created");
|
|
51
|
+
assert.ok(fs.existsSync(path.join(workspaceRoot, ".ling", "spec", "profiles", "codex", "AGENTS.spec.md")), "spec profiles should be created");
|
|
52
|
+
assert.ok(
|
|
53
|
+
fs.existsSync(path.join(workspaceRoot, ".ling", "spec", "profiles", "codex", "ling.spec.rules.md")),
|
|
54
|
+
"spec profile rules should be created",
|
|
55
|
+
);
|
|
56
|
+
assert.ok(fs.existsSync(path.join(workspaceRoot, ".ling", "spec", "profiles", "gemini", "GEMINI.spec.md")), "spec profiles should be created");
|
|
57
|
+
assert.ok(fs.existsSync(path.join(workspaceRoot, "docs", "reviews")), "docs/reviews should exist");
|
|
58
|
+
assert.ok(fs.existsSync(path.join(workspaceRoot, "docs", "handoff")), "docs/handoff should exist");
|
|
59
|
+
|
|
60
|
+
const doctorResult = runCli(["spec", "doctor", "--path", workspaceRoot, "--quiet"], { env });
|
|
61
|
+
assert.strictEqual(doctorResult.status, 0, doctorResult.stderr || doctorResult.stdout);
|
|
62
|
+
assert.strictEqual((doctorResult.stdout || "").trim(), "installed");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("spec init --csv-only should generate issues.csv and rely on global spec assets", () => {
|
|
66
|
+
const env = {
|
|
67
|
+
LING_GLOBAL_ROOT: tempRoot,
|
|
68
|
+
LING_INDEX_PATH: path.join(tempRoot, "workspaces.json"),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const enableResult = runCli(["spec", "enable", "--target", "codex", "--quiet"], { env });
|
|
72
|
+
assert.strictEqual(enableResult.status, 0, enableResult.stderr || enableResult.stdout);
|
|
73
|
+
|
|
74
|
+
const initResult = runCli(["spec", "init", "--path", workspaceRoot, "--csv-only", "--quiet"], { env });
|
|
75
|
+
assert.strictEqual(initResult.status, 0, initResult.stderr || initResult.stdout);
|
|
76
|
+
|
|
77
|
+
assert.ok(fs.existsSync(path.join(workspaceRoot, "issues.csv")), "issues.csv should be created");
|
|
78
|
+
assert.ok(fs.existsSync(path.join(workspaceRoot, "docs", "reviews")), "docs/reviews should exist");
|
|
79
|
+
assert.ok(fs.existsSync(path.join(workspaceRoot, "docs", "handoff")), "docs/handoff should exist");
|
|
80
|
+
assert.ok(!fs.existsSync(path.join(workspaceRoot, ".ling", "spec")), "spec assets should not be created in csv-only mode");
|
|
81
|
+
|
|
82
|
+
const doctorResult = runCli(["spec", "doctor", "--path", workspaceRoot, "--quiet"], { env });
|
|
83
|
+
assert.strictEqual(doctorResult.status, 0, doctorResult.stderr || doctorResult.stdout);
|
|
84
|
+
assert.strictEqual((doctorResult.stdout || "").trim(), "installed");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("spec init without --path should initialize default spec-workspace and include targets", () => {
|
|
88
|
+
const nonTempRoot = fs.mkdtempSync(path.join(REPO_ROOT, ".tmp-ling-spec-global-"));
|
|
89
|
+
const indexPath = path.join(nonTempRoot, "workspaces.json");
|
|
90
|
+
const env = {
|
|
91
|
+
LING_GLOBAL_ROOT: nonTempRoot,
|
|
92
|
+
LING_INDEX_PATH: indexPath,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const initResult = runCli(["spec", "init", "--spec-workspace", "--quiet"], { env });
|
|
97
|
+
assert.strictEqual(initResult.status, 0, initResult.stderr || initResult.stdout);
|
|
98
|
+
|
|
99
|
+
const defaultWorkspace = path.join(nonTempRoot, ".ling", "spec-workspace");
|
|
100
|
+
assert.ok(fs.existsSync(path.join(defaultWorkspace, "issues.csv")), "default workspace should include issues.csv");
|
|
101
|
+
assert.ok(
|
|
102
|
+
fs.existsSync(path.join(defaultWorkspace, ".ling", "spec", "templates", "driver-prompt.md")),
|
|
103
|
+
"default workspace should include spec templates",
|
|
104
|
+
);
|
|
105
|
+
assert.ok(fs.existsSync(path.join(defaultWorkspace, ".agent")), "default workspace should include gemini .agent");
|
|
106
|
+
assert.ok(fs.existsSync(path.join(defaultWorkspace, ".agents")), "default workspace should include codex .agents");
|
|
107
|
+
|
|
108
|
+
const doctorResult = runCli(["spec", "doctor", "--spec-workspace", "--quiet"], { env });
|
|
109
|
+
assert.strictEqual(doctorResult.status, 0, doctorResult.stderr || doctorResult.stdout);
|
|
110
|
+
assert.strictEqual((doctorResult.stdout || "").trim(), "installed");
|
|
111
|
+
|
|
112
|
+
assert.ok(fs.existsSync(indexPath), "workspace index should be created for non-temp global root");
|
|
113
|
+
const index = JSON.parse(fs.readFileSync(indexPath, "utf8"));
|
|
114
|
+
assert.ok(
|
|
115
|
+
(index.workspaces || []).some((workspace) => workspace && workspace.path === defaultWorkspace),
|
|
116
|
+
"default spec-workspace should be registered into index",
|
|
117
|
+
);
|
|
118
|
+
} finally {
|
|
119
|
+
fs.rmSync(nonTempRoot, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("spec doctor should report broken when multiple tasks are in 进行中", () => {
|
|
124
|
+
const env = {
|
|
125
|
+
LING_GLOBAL_ROOT: tempRoot,
|
|
126
|
+
LING_INDEX_PATH: path.join(tempRoot, "workspaces.json"),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const initResult = runCli(["spec", "init", "--path", workspaceRoot, "--quiet"], { env });
|
|
130
|
+
assert.strictEqual(initResult.status, 0, initResult.stderr || initResult.stdout);
|
|
131
|
+
|
|
132
|
+
const issuesPath = path.join(workspaceRoot, "issues.csv");
|
|
133
|
+
fs.writeFileSync(
|
|
134
|
+
issuesPath,
|
|
135
|
+
[
|
|
136
|
+
"ID,标题,内容,验收标准,审查要求,状态,标签",
|
|
137
|
+
"A,任务A,内容A,验收A,审查A,进行中,高优先级",
|
|
138
|
+
"B,任务B,内容B,验收B,审查B,进行中,高优先级",
|
|
139
|
+
"",
|
|
140
|
+
].join("\n"),
|
|
141
|
+
"utf8",
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const doctorResult = runCli(["spec", "doctor", "--path", workspaceRoot, "--quiet"], { env });
|
|
145
|
+
assert.strictEqual(doctorResult.status, 1, doctorResult.stderr || doctorResult.stdout);
|
|
146
|
+
assert.strictEqual((doctorResult.stdout || "").trim(), "broken");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("spec doctor should report broken when issues.csv is missing but spec directory exists", () => {
|
|
150
|
+
const env = {
|
|
151
|
+
LING_GLOBAL_ROOT: tempRoot,
|
|
152
|
+
LING_INDEX_PATH: path.join(tempRoot, "workspaces.json"),
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const initResult = runCli(["spec", "init", "--path", workspaceRoot, "--quiet"], { env });
|
|
156
|
+
assert.strictEqual(initResult.status, 0, initResult.stderr || initResult.stdout);
|
|
157
|
+
|
|
158
|
+
fs.rmSync(path.join(workspaceRoot, "issues.csv"), { force: true });
|
|
159
|
+
|
|
160
|
+
const doctorResult = runCli(["spec", "doctor", "--path", workspaceRoot, "--quiet"], { env });
|
|
161
|
+
assert.strictEqual(doctorResult.status, 1, doctorResult.stderr || doctorResult.stdout);
|
|
162
|
+
assert.strictEqual((doctorResult.stdout || "").trim(), "broken");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("spec init should support --branch for spec assets", () => {
|
|
166
|
+
const gitCheck = spawnSync("git", ["--version"], { encoding: "utf8" });
|
|
167
|
+
if (gitCheck.status !== 0) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const sourceRepo = path.join(tempRoot, "spec-source-repo");
|
|
172
|
+
fs.mkdirSync(sourceRepo, { recursive: true });
|
|
173
|
+
|
|
174
|
+
const runGit = (args) =>
|
|
175
|
+
spawnSync("git", args, {
|
|
176
|
+
cwd: sourceRepo,
|
|
177
|
+
encoding: "utf8",
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const initRes = runGit(["init", "--initial-branch", "main"]);
|
|
181
|
+
if (initRes.status !== 0) {
|
|
182
|
+
assert.strictEqual(runGit(["init"]).status, 0);
|
|
183
|
+
assert.strictEqual(runGit(["checkout", "-b", "main"]).status, 0);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const specRoot = path.join(sourceRepo, ".spec");
|
|
187
|
+
const templatesDir = path.join(specRoot, "templates");
|
|
188
|
+
const referencesDir = path.join(specRoot, "references");
|
|
189
|
+
const profilesCodexDir = path.join(specRoot, "profiles", "codex");
|
|
190
|
+
const profilesGeminiDir = path.join(specRoot, "profiles", "gemini");
|
|
191
|
+
fs.mkdirSync(templatesDir, { recursive: true });
|
|
192
|
+
fs.mkdirSync(referencesDir, { recursive: true });
|
|
193
|
+
fs.mkdirSync(profilesCodexDir, { recursive: true });
|
|
194
|
+
fs.mkdirSync(profilesGeminiDir, { recursive: true });
|
|
195
|
+
|
|
196
|
+
fs.writeFileSync(path.join(templatesDir, "issues.template.csv"), "ID,状态\nX,未开始\n", "utf8");
|
|
197
|
+
fs.writeFileSync(path.join(templatesDir, "driver-prompt.md"), "branch driver prompt", "utf8");
|
|
198
|
+
fs.writeFileSync(path.join(templatesDir, "review-report.md"), "branch review report", "utf8");
|
|
199
|
+
fs.writeFileSync(path.join(templatesDir, "phase-acceptance.md"), "branch acceptance", "utf8");
|
|
200
|
+
fs.writeFileSync(path.join(templatesDir, "handoff.md"), "branch handoff", "utf8");
|
|
201
|
+
|
|
202
|
+
fs.writeFileSync(path.join(referencesDir, "README.md"), "branch readme", "utf8");
|
|
203
|
+
fs.writeFileSync(path.join(referencesDir, "harness-engineering-digest.md"), "branch digest", "utf8");
|
|
204
|
+
fs.writeFileSync(path.join(referencesDir, "gda-framework.md"), "branch gda", "utf8");
|
|
205
|
+
fs.writeFileSync(path.join(referencesDir, "cse-quickstart.md"), "branch quickstart", "utf8");
|
|
206
|
+
|
|
207
|
+
fs.writeFileSync(path.join(profilesCodexDir, "AGENTS.spec.md"), "branch codex agents", "utf8");
|
|
208
|
+
fs.writeFileSync(path.join(profilesCodexDir, "ling.spec.rules.md"), "branch codex rules", "utf8");
|
|
209
|
+
fs.writeFileSync(path.join(profilesGeminiDir, "GEMINI.spec.md"), "branch gemini profile", "utf8");
|
|
210
|
+
|
|
211
|
+
assert.strictEqual(runGit(["add", "."]).status, 0);
|
|
212
|
+
assert.strictEqual(
|
|
213
|
+
runGit(["-c", "user.name=ling-test", "-c", "user.email=ling-test@example.com", "commit", "-m", "spec assets"]).status,
|
|
214
|
+
0,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const env = {
|
|
218
|
+
LING_GLOBAL_ROOT: tempRoot,
|
|
219
|
+
LING_REPO_URL: sourceRepo,
|
|
220
|
+
LING_INDEX_PATH: path.join(tempRoot, "workspaces.json"),
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const initResult = runCli(["spec", "init", "--path", workspaceRoot, "--branch", "main", "--quiet"], { env });
|
|
224
|
+
assert.strictEqual(initResult.status, 0, initResult.stderr || initResult.stdout);
|
|
225
|
+
|
|
226
|
+
const installedPrompt = path.join(workspaceRoot, ".ling", "spec", "templates", "driver-prompt.md");
|
|
227
|
+
assert.strictEqual(fs.readFileSync(installedPrompt, "utf8"), "branch driver prompt");
|
|
228
|
+
|
|
229
|
+
const doctorResult = runCli(["spec", "doctor", "--path", workspaceRoot, "--quiet"], { env });
|
|
230
|
+
assert.strictEqual(doctorResult.status, 0, doctorResult.stderr || doctorResult.stdout);
|
|
231
|
+
assert.strictEqual((doctorResult.stdout || "").trim(), "installed");
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -51,11 +51,14 @@ describe("Spec Profile", () => {
|
|
|
51
51
|
const stateFile = path.join(tempRoot, ".ling", "spec", "state.json");
|
|
52
52
|
const templatesDir = path.join(tempRoot, ".ling", "spec", "templates");
|
|
53
53
|
const referencesDir = path.join(tempRoot, ".ling", "spec", "references");
|
|
54
|
+
const profilesDir = path.join(tempRoot, ".ling", "spec", "profiles");
|
|
54
55
|
|
|
55
56
|
assert.ok(fs.existsSync(codexSkill), "missing installed codex spec skill");
|
|
56
57
|
assert.ok(fs.existsSync(stateFile), "missing spec state");
|
|
57
58
|
assert.ok(fs.existsSync(path.join(templatesDir, "issues.template.csv")), "missing spec template");
|
|
58
59
|
assert.ok(fs.existsSync(path.join(referencesDir, "harness-engineering-digest.md")), "missing spec reference");
|
|
60
|
+
assert.ok(fs.existsSync(path.join(profilesDir, "codex", "AGENTS.spec.md")), "missing spec profile");
|
|
61
|
+
assert.ok(fs.existsSync(path.join(profilesDir, "codex", "ling.spec.rules.md")), "missing spec profile rules");
|
|
59
62
|
|
|
60
63
|
const statusResult = runCli(["spec", "status", "--quiet"], { env });
|
|
61
64
|
assert.strictEqual(statusResult.status, 0);
|
|
@@ -67,6 +70,7 @@ describe("Spec Profile", () => {
|
|
|
67
70
|
assert.ok(!fs.existsSync(stateFile), "spec state should be removed after final disable");
|
|
68
71
|
assert.ok(!fs.existsSync(templatesDir), "spec templates should be removed after final disable");
|
|
69
72
|
assert.ok(!fs.existsSync(referencesDir), "spec references should be removed after final disable");
|
|
73
|
+
assert.ok(!fs.existsSync(profilesDir), "spec profiles should be removed after final disable");
|
|
70
74
|
});
|
|
71
75
|
|
|
72
76
|
test("spec disable should restore pre-existing skill backup", () => {
|
|
@@ -83,4 +87,84 @@ describe("Spec Profile", () => {
|
|
|
83
87
|
assert.strictEqual(disableResult.status, 0, disableResult.stderr || disableResult.stdout);
|
|
84
88
|
assert.strictEqual(fs.readFileSync(path.join(skillDir, "SKILL.md"), "utf8"), "legacy skill");
|
|
85
89
|
});
|
|
90
|
+
|
|
91
|
+
test("spec enable should repair missing assets and skills when state exists", () => {
|
|
92
|
+
const env = { LING_GLOBAL_ROOT: tempRoot };
|
|
93
|
+
|
|
94
|
+
const enableResult = runCli(["spec", "enable", "--target", "codex", "--quiet"], { env });
|
|
95
|
+
assert.strictEqual(enableResult.status, 0, enableResult.stderr || enableResult.stdout);
|
|
96
|
+
|
|
97
|
+
const templatesDir = path.join(tempRoot, ".ling", "spec", "templates");
|
|
98
|
+
const codexSkillDir = path.join(tempRoot, ".codex", "skills", "harness-engineering");
|
|
99
|
+
fs.rmSync(templatesDir, { recursive: true, force: true });
|
|
100
|
+
fs.rmSync(codexSkillDir, { recursive: true, force: true });
|
|
101
|
+
|
|
102
|
+
const brokenResult = runCli(["spec", "status", "--quiet"], { env });
|
|
103
|
+
assert.strictEqual(brokenResult.status, 1);
|
|
104
|
+
assert.strictEqual((brokenResult.stdout || "").trim(), "broken");
|
|
105
|
+
|
|
106
|
+
const repairResult = runCli(["spec", "enable", "--target", "codex", "--quiet"], { env });
|
|
107
|
+
assert.strictEqual(repairResult.status, 0, repairResult.stderr || repairResult.stdout);
|
|
108
|
+
|
|
109
|
+
const repairedStatus = runCli(["spec", "status", "--quiet"], { env });
|
|
110
|
+
assert.strictEqual(repairedStatus.status, 0);
|
|
111
|
+
assert.strictEqual((repairedStatus.stdout || "").trim(), "installed");
|
|
112
|
+
|
|
113
|
+
assert.ok(fs.existsSync(path.join(templatesDir, "issues.template.csv")), "templates should be repaired");
|
|
114
|
+
assert.ok(fs.existsSync(path.join(codexSkillDir, "SKILL.md")), "spec skill should be repaired");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("spec enable should install antigravity skills independently", () => {
|
|
118
|
+
const env = { LING_GLOBAL_ROOT: tempRoot };
|
|
119
|
+
|
|
120
|
+
const enableResult = runCli(["spec", "enable", "--target", "antigravity", "--quiet"], { env });
|
|
121
|
+
assert.strictEqual(enableResult.status, 0, enableResult.stderr || enableResult.stdout);
|
|
122
|
+
|
|
123
|
+
const antigravitySkill = path.join(tempRoot, ".gemini", "antigravity", "skills", "harness-engineering", "SKILL.md");
|
|
124
|
+
const geminiSkill = path.join(tempRoot, ".gemini", "skills", "harness-engineering", "SKILL.md");
|
|
125
|
+
assert.ok(fs.existsSync(antigravitySkill), "missing installed antigravity spec skill");
|
|
126
|
+
assert.ok(!fs.existsSync(geminiSkill), "antigravity spec enable should not install gemini skill");
|
|
127
|
+
|
|
128
|
+
const statusResult = runCli(["spec", "status", "--quiet"], { env });
|
|
129
|
+
assert.strictEqual(statusResult.status, 0);
|
|
130
|
+
assert.strictEqual((statusResult.stdout || "").trim(), "installed");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("spec status should report broken when an asset file is missing", () => {
|
|
134
|
+
const env = { LING_GLOBAL_ROOT: tempRoot };
|
|
135
|
+
|
|
136
|
+
const enableResult = runCli(["spec", "enable", "--target", "codex", "--quiet"], { env });
|
|
137
|
+
assert.strictEqual(enableResult.status, 0, enableResult.stderr || enableResult.stdout);
|
|
138
|
+
|
|
139
|
+
const driverPrompt = path.join(tempRoot, ".ling", "spec", "templates", "driver-prompt.md");
|
|
140
|
+
assert.ok(fs.existsSync(driverPrompt), "driver-prompt.md should exist after enable");
|
|
141
|
+
fs.rmSync(driverPrompt, { force: true });
|
|
142
|
+
|
|
143
|
+
const statusResult = runCli(["spec", "status", "--quiet"], { env });
|
|
144
|
+
assert.strictEqual(statusResult.status, 1);
|
|
145
|
+
assert.strictEqual((statusResult.stdout || "").trim(), "broken");
|
|
146
|
+
|
|
147
|
+
const repairResult = runCli(["spec", "enable", "--target", "codex", "--quiet"], { env });
|
|
148
|
+
assert.strictEqual(repairResult.status, 0, repairResult.stderr || repairResult.stdout);
|
|
149
|
+
assert.ok(fs.existsSync(driverPrompt), "driver-prompt.md should be repaired");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("spec status should report broken when state.json is missing but assets exist", () => {
|
|
153
|
+
const env = { LING_GLOBAL_ROOT: tempRoot };
|
|
154
|
+
|
|
155
|
+
const templatesDir = path.join(tempRoot, ".ling", "spec", "templates");
|
|
156
|
+
fs.mkdirSync(templatesDir, { recursive: true });
|
|
157
|
+
fs.writeFileSync(path.join(templatesDir, "issues.template.csv"), "sentinel", "utf8");
|
|
158
|
+
|
|
159
|
+
const statusResult = runCli(["spec", "status", "--quiet"], { env });
|
|
160
|
+
assert.strictEqual(statusResult.status, 1);
|
|
161
|
+
assert.strictEqual((statusResult.stdout || "").trim(), "broken");
|
|
162
|
+
|
|
163
|
+
const repairResult = runCli(["spec", "enable", "--target", "codex", "--quiet"], { env });
|
|
164
|
+
assert.strictEqual(repairResult.status, 0, repairResult.stderr || repairResult.stdout);
|
|
165
|
+
|
|
166
|
+
const repairedStatus = runCli(["spec", "status", "--quiet"], { env });
|
|
167
|
+
assert.strictEqual(repairedStatus.status, 0);
|
|
168
|
+
assert.strictEqual((repairedStatus.stdout || "").trim(), "installed");
|
|
169
|
+
});
|
|
86
170
|
});
|
|
@@ -144,6 +144,37 @@ describe('Standards Compliance', () => {
|
|
|
144
144
|
assert.ok(!content.includes('$HOME/.agents/skills/'), 'should not contain deprecated global path: $HOME/.agents/skills/');
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
+
test('README spec section should reflect implemented spec commands', () => {
|
|
148
|
+
const file = path.resolve('README.md');
|
|
149
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
150
|
+
|
|
151
|
+
assert.ok(content.includes('ling spec init'), 'README should mention ling spec init');
|
|
152
|
+
assert.ok(content.includes('ling spec doctor'), 'README should mention ling spec doctor');
|
|
153
|
+
assert.ok(content.includes('~/.ling/spec/profiles/'), 'README should mention spec profiles path');
|
|
154
|
+
assert.ok(!content.includes('本版本尚未开放'), 'README should not claim spec project commands are unavailable');
|
|
155
|
+
assert.ok(!content.includes('spec init/remove/doctor'), 'README should not mention unsupported spec remove subcommand');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('docs should describe gemini and antigravity as separate command targets', () => {
|
|
159
|
+
const readme = fs.readFileSync(path.resolve('README.md'), 'utf8');
|
|
160
|
+
const tech = fs.readFileSync(path.resolve('docs/TECH.md'), 'utf8');
|
|
161
|
+
|
|
162
|
+
assert.ok(readme.includes('ling init --target antigravity'), 'README should document project antigravity target');
|
|
163
|
+
assert.ok(readme.includes('ling global sync --target antigravity'), 'README should document global antigravity target');
|
|
164
|
+
assert.ok(tech.includes('`--target gemini` 只写入 gemini-cli;`--target antigravity` 只写入 antigravity'), 'TECH should describe split global targets');
|
|
165
|
+
assert.ok(!tech.includes('gemini 会同时写入 gemini-cli 与 antigravity'), 'TECH should not describe gemini as bundled antigravity target');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('gitignore should whitelist curated reference materials only', () => {
|
|
169
|
+
const rootIgnore = fs.readFileSync(path.resolve('.gitignore'), 'utf8');
|
|
170
|
+
const referenceIgnore = fs.readFileSync(path.resolve('reference/.gitignore'), 'utf8');
|
|
171
|
+
|
|
172
|
+
assert.ok(rootIgnore.includes('!reference/official/**'), 'root .gitignore should keep reference/official tracked');
|
|
173
|
+
assert.ok(rootIgnore.includes('!reference/docs-archive/**'), 'root .gitignore should keep reference/docs-archive tracked');
|
|
174
|
+
assert.ok(referenceIgnore.includes('!official/**'), 'reference/.gitignore should keep official subtree tracked');
|
|
175
|
+
assert.ok(referenceIgnore.includes('!docs-archive/**'), 'reference/.gitignore should keep docs-archive subtree tracked');
|
|
176
|
+
});
|
|
177
|
+
|
|
147
178
|
test('.agents script files should stay identical to reference snapshot', { skip: !HAS_REF_SCRIPTS_ROOT }, () => {
|
|
148
179
|
const refScriptsRoot = REF_SCRIPTS_ROOT;
|
|
149
180
|
const mismatches = [];
|