musubix6 0.1.2 → 0.1.3
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/.github/skills/lean-sdd-tdd/SKILL.md +38 -0
- package/.github/skills/lean-sdd-tdd/references/config.md +1 -0
- package/.github/skills/lean-sdd-tdd/scripts/diagnostics-eslint.mjs +50 -0
- package/.github/skills/lean-sdd-tdd/scripts/diagnostics-native.mjs +85 -0
- package/.github/skills/lean-sdd-tdd/scripts/diagnostics-profiles.test.mjs +610 -0
- package/.github/skills/lean-sdd-tdd/scripts/diagnostics-python.mjs +133 -0
- package/.github/skills/lean-sdd-tdd/scripts/diagnostics-python.py +108 -0
- package/.github/skills/lean-sdd-tdd/scripts/diagnostics-rust.mjs +120 -0
- package/.github/skills/lean-sdd-tdd/scripts/diagnostics-typescript.mjs +53 -0
- package/.github/skills/lean-sdd-tdd/scripts/diagnostics-worker.mjs +19 -0
- package/.github/skills/lean-sdd-tdd/scripts/diagnostics.mjs +212 -0
- package/.github/skills/lean-sdd-tdd/scripts/diagnostics.test.mjs +505 -0
- package/.github/skills/lean-sdd-tdd/scripts/sdd.mjs +9 -3
- package/.github/skills/lean-sdd-tdd/scripts/sdd.test.mjs +6 -2
- package/README-ja.md +318 -18
- package/README.md +296 -20
- package/package.json +4 -2
- package/.github/skills/japanese-prose/scripts/__pycache__/core.cpython-312.pyc +0 -0
- package/.github/skills/japanese-prose/scripts/__pycache__/lint.cpython-312.pyc +0 -0
- package/.github/skills/japanese-prose/scripts/__pycache__/outline.cpython-312.pyc +0 -0
- package/.github/skills/japanese-prose/scripts/__pycache__/terms.cpython-312.pyc +0 -0
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
import { test, after } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import * as core from './diagnostics.mjs';
|
|
10
|
+
import { diagnoseESLint } from './diagnostics-eslint.mjs';
|
|
11
|
+
import { nativeBudget } from './diagnostics-native.mjs';
|
|
12
|
+
import { diagnosePython } from './diagnostics-python.mjs';
|
|
13
|
+
|
|
14
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const repo = path.resolve(here, '../../../..');
|
|
16
|
+
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'musubix6-profiles-test-'));
|
|
17
|
+
const { NODE_TEST_CONTEXT, ...baseEnv } = process.env;
|
|
18
|
+
after(() => fs.rmSync(scratch, { recursive: true, force: true }));
|
|
19
|
+
function write(root, name, value) {
|
|
20
|
+
const file = path.join(root, name);
|
|
21
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
22
|
+
fs.writeFileSync(file, value);
|
|
23
|
+
}
|
|
24
|
+
function project() { return fs.mkdtempSync(path.join(scratch, 'project-')); }
|
|
25
|
+
function snapshot(root) {
|
|
26
|
+
const files = [];
|
|
27
|
+
function walk(dir) {
|
|
28
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
29
|
+
const file = path.join(dir, entry.name);
|
|
30
|
+
if (entry.isSymbolicLink()) files.push([path.relative(root, file), fs.readlinkSync(file)]);
|
|
31
|
+
else if (entry.isDirectory()) walk(file);
|
|
32
|
+
else files.push([path.relative(root, file), createHash('sha256').update(fs.readFileSync(file)).digest('hex')]);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
walk(root);
|
|
36
|
+
return files.sort((a, b) => a[0].localeCompare(b[0]));
|
|
37
|
+
}
|
|
38
|
+
function cli(root, profile, extra = [], env = {}, entry = path.join(here, 'sdd.mjs'), prefix = []) {
|
|
39
|
+
const result = spawnSync(process.execPath, [entry, ...prefix, 'diagnostics', '--profile', profile, '--json', ...extra], {
|
|
40
|
+
cwd: root, env: { ...baseEnv, ...env }, encoding: 'utf8', timeout: 30000, maxBuffer: 20 * 1024 * 1024,
|
|
41
|
+
});
|
|
42
|
+
assert.ifError(result.error);
|
|
43
|
+
assert.equal(result.signal, null, result.stderr);
|
|
44
|
+
const value = JSON.parse(result.stdout);
|
|
45
|
+
assert.equal(value.exitCode, result.status, result.stderr);
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
function complete(value) {
|
|
49
|
+
assert.equal(value.status, 'complete', JSON.stringify(value));
|
|
50
|
+
assert.equal(value.reason, null);
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
function incomplete(value, reason) {
|
|
54
|
+
assert.equal(value.status, 'incomplete', JSON.stringify(value));
|
|
55
|
+
assert.ok(value.reason);
|
|
56
|
+
if (reason) assert.match(value.reason, reason);
|
|
57
|
+
assert.deepEqual(value.files, { included: null, checked: null, ignored: null });
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
function javascript() {
|
|
61
|
+
const root = project();
|
|
62
|
+
write(root, 'package.json', '{"private":true}\n');
|
|
63
|
+
write(root, 'eslint.config.cjs', 'module.exports=[{ignores:["ignored.js"]},{files:["**/*.{js,jsx,mjs,cjs}"],languageOptions:{parserOptions:{ecmaFeatures:{jsx:true}}},rules:{"no-debugger":"error","no-unused-vars":"warn"}}];\n');
|
|
64
|
+
write(root, 'a.js', 'export const value = 1;\n');
|
|
65
|
+
fs.mkdirSync(path.join(root, 'node_modules'));
|
|
66
|
+
fs.symlinkSync(path.join(repo, 'node_modules/eslint'), path.join(root, 'node_modules/eslint'), 'dir');
|
|
67
|
+
return root;
|
|
68
|
+
}
|
|
69
|
+
function toolVenv() {
|
|
70
|
+
const venv = process.env.MUSUBIX_DIAGNOSTICS_TEST_VENV;
|
|
71
|
+
assert.ok(venv && fs.existsSync(path.join(venv, 'bin/python')), 'Set MUSUBIX_DIAGNOSTICS_TEST_VENV to the pinned developer-tool venv; do not install tools inside diagnostics');
|
|
72
|
+
return fs.realpathSync(venv);
|
|
73
|
+
}
|
|
74
|
+
function python() {
|
|
75
|
+
const root = project();
|
|
76
|
+
fs.symlinkSync(toolVenv(), path.join(root, '.venv'), 'dir');
|
|
77
|
+
write(root, 'a.py', 'value: int = 1\n');
|
|
78
|
+
return root;
|
|
79
|
+
}
|
|
80
|
+
function rust() {
|
|
81
|
+
const root = project(), home = project();
|
|
82
|
+
write(root, 'Cargo.toml', '[package]\nname="profile-fixture"\nversion="0.0.0"\nedition="2024"\n');
|
|
83
|
+
write(root, 'Cargo.lock', 'version = 4\n\n[[package]]\nname = "profile-fixture"\nversion = "0.0.0"\n');
|
|
84
|
+
write(root, 'src/lib.rs', 'pub fn value() -> i32 { 1 }\n');
|
|
85
|
+
write(root, '.cargo/config.toml', '[build]\ntarget-dir="project-target"\nbuild-dir="project-build"\n[cache]\nauto-clean-frequency="always"\n');
|
|
86
|
+
return { root, home, env: { CARGO_HOME: home, RUSTUP_AUTO_INSTALL: '0' } };
|
|
87
|
+
}
|
|
88
|
+
function provider(root, name, env) {
|
|
89
|
+
const data = cli(root, 'python', [], env);
|
|
90
|
+
assert.deepEqual(data.providers.map(p => p.source), ['ruff', 'mypy'], JSON.stringify(data));
|
|
91
|
+
return data.providers.find(p => p.source === name);
|
|
92
|
+
}
|
|
93
|
+
function isolatedPython({ ruff = null, version = '0.17.1', mypy = null, mypyVersion = '2.4.0' } = {}) {
|
|
94
|
+
const root = project(), venv = path.join(root, '.venv');
|
|
95
|
+
const created = spawnSync(path.join(toolVenv(), 'bin/python'), ['-B', '-I', '-m', 'venv', '--without-pip', venv], { encoding: 'utf8' });
|
|
96
|
+
assert.equal(created.status, 0, created.stderr);
|
|
97
|
+
const source = path.join(toolVenv(), 'lib/python3.14/site-packages');
|
|
98
|
+
const site = path.join(venv, 'lib/python3.14/site-packages');
|
|
99
|
+
fs.symlinkSync(path.join(toolVenv(), 'bin/ruff'), path.join(venv, 'bin/ruff'), 'file');
|
|
100
|
+
for (const entry of fs.readdirSync(source)) {
|
|
101
|
+
if (ruff !== null && (entry === 'ruff' || /^ruff-.*\.dist-info$/.test(entry))) continue;
|
|
102
|
+
if (mypy !== null && (entry === 'mypy' || /^mypy-.*\.dist-info$/.test(entry))) continue;
|
|
103
|
+
fs.symlinkSync(path.join(source, entry), path.join(site, entry), fs.statSync(path.join(source, entry)).isDirectory() ? 'dir' : 'file');
|
|
104
|
+
}
|
|
105
|
+
for (const [name, body, selectedVersion] of [['ruff', ruff, version], ['mypy', mypy, mypyVersion]]) {
|
|
106
|
+
if (body === null) continue;
|
|
107
|
+
write(site, `${name}/__init__.py`, '');
|
|
108
|
+
write(site, `${name}/__main__.py`, body);
|
|
109
|
+
write(site, `${name}-${selectedVersion}.dist-info/METADATA`, `Name: ${name}\nVersion: ${selectedVersion}\n`);
|
|
110
|
+
}
|
|
111
|
+
write(root, 'a.py', 'missing\n');
|
|
112
|
+
return { root, site };
|
|
113
|
+
}
|
|
114
|
+
function fakeRust() {
|
|
115
|
+
const fixture = rust(), tools = project(), events = path.join(project(), 'events.jsonl');
|
|
116
|
+
const metadata = { packages: [{ id: 'fixture-id', manifest_path: path.join(fixture.root, 'Cargo.toml'),
|
|
117
|
+
targets: [{ src_path: path.join(fixture.root, 'src/lib.rs') }] }], workspace_members: ['fixture-id'], workspace_root: fixture.root };
|
|
118
|
+
const cargo = `#!${process.execPath}\nconst fs=require("fs"); const a=process.argv.slice(2);fs.appendFileSync(${JSON.stringify(events)},JSON.stringify({a, cwd:process.cwd(),rustc:process.env.RUSTC,autoInstall:process.env.RUSTUP_AUTO_INSTALL})+"\\n");if(a[0]==="-V"){console.log("cargo "+(process.env.FIXTURE_CARGO_VERSION||"1.99.0")+" (fixture)");}else if(a[0]==="metadata"){console.log(${JSON.stringify(JSON.stringify(metadata))});}else{const t=a[a.indexOf("--target-dir")+1]; if(!t||!a.includes('--offline')||!a.includes('--locked')||!a.includes('--all-targets')||!a.includes('cache.auto-clean-frequency="never"')||!a.includes("build.build-dir="+JSON.stringify(require("path").join(t,"intermediates"))))throw Error("missing required Cargo policy");process.stdout.write(process.env.FIXTURE_CARGO_MESSAGES||"");process.stderr.write(process.env.FIXTURE_CARGO_STDERR||"");process.exitCode=Number(process.env.FIXTURE_CARGO_EXIT||0);}\n`;
|
|
119
|
+
write(tools, 'cargo', cargo);
|
|
120
|
+
write(tools, 'rustc', `#!${process.execPath}\nconsole.log("rustc 1.99.0 (fixture)\\nrelease: 1.99.0");\n`);
|
|
121
|
+
fs.chmodSync(path.join(tools, 'cargo'), 0o755);
|
|
122
|
+
fs.chmodSync(path.join(tools, 'rustc'), 0o755);
|
|
123
|
+
return { ...fixture, tools, events, env: { ...fixture.env, PATH: tools + path.delimiter + baseEnv.PATH } };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** @id TEST-DIAGCLI-006 @verifies REQ-DIAGCLI-001 REQ-DIAGCLI-003 */
|
|
127
|
+
test('TEST-DIAGCLI-006 explicit profiles, invalid input and all144 policy rows', () => {
|
|
128
|
+
const root = javascript();
|
|
129
|
+
assert.equal(cli(root, 'javascript').exitCode, 0);
|
|
130
|
+
for (const args of [['--profile', 'javascript'], ['--profile', 'python'], ['extra']]) {
|
|
131
|
+
const value = cli(root, 'javascript', args);
|
|
132
|
+
assert.equal(value.exitCode, 2);
|
|
133
|
+
assert.equal(value.root, null);
|
|
134
|
+
assert.deepEqual(value.providers, []);
|
|
135
|
+
}
|
|
136
|
+
for (const name of ['', 'unknown', 'JavaScript']) {
|
|
137
|
+
const value = cli(root, name);
|
|
138
|
+
assert.equal(value.exitCode, 2);
|
|
139
|
+
assert.deepEqual(value.providers, []);
|
|
140
|
+
}
|
|
141
|
+
assert.equal(cli(root, 'python', ['--root', '/definitely/not/a/project']).profile, 'python');
|
|
142
|
+
const states = ['C00', 'C0W', 'CE0', 'CEW', 'I00', 'I0W', 'IE0', 'IEW'];
|
|
143
|
+
const rows = ['PPFFIIII', 'PPFFIIII', 'FFFFIIII', 'FFFFIIII', 'IIIIIIII', 'IIIIIIII', 'IIIIIIII', 'IIIIIIII'];
|
|
144
|
+
const sets = { typescript: ['typescript', 'eslint'], javascript: ['eslint'], python: ['ruff', 'mypy'], rust: ['cargo-check'] };
|
|
145
|
+
let checked = 0;
|
|
146
|
+
const value = (source, state) => ({ source, status: state[0] === 'C' ? 'complete' : 'incomplete', reason: 'fixture',
|
|
147
|
+
diagnostics: [...(state[1] === 'E' ? [{ severity: 'error' }] : []), ...(state[2] === 'W' ? [{ severity: 'warning' }] : [])] });
|
|
148
|
+
for (const required of Object.values(sets)) {
|
|
149
|
+
for (let i = 0; i < 8; i++) for (let j = 0; j < (required.length === 2 ? 8 : 1); j++) {
|
|
150
|
+
const providers = required.map((source, index) => value(source, states[index ? j : i]));
|
|
151
|
+
const expected = required.length === 2 ? rows[i][j] : 'PPFFIIII'[i];
|
|
152
|
+
assert.equal(core.aggregate(providers, required).status, { P: 'PASS', F: 'FAIL', I: 'INCOMPLETE' }[expected]);
|
|
153
|
+
checked++;
|
|
154
|
+
}
|
|
155
|
+
const valid = required.map(source => value(source, 'C00'));
|
|
156
|
+
for (const providers of [[], valid.slice(1), [...valid, valid[0]], [...valid, value('unexpected', 'C00')]]) assert.equal(core.aggregate(providers, required).exitCode, 2);
|
|
157
|
+
assert.equal(core.aggregate(valid, []).exitCode, 2);
|
|
158
|
+
}
|
|
159
|
+
assert.equal(checked, 144);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
/** @id TEST-DIAGJS-001 @verifies REQ-DIAGJS-001 */
|
|
163
|
+
test('TEST-DIAGJS-001 root-only ESLint without TypeScript admission', async () => {
|
|
164
|
+
const root = javascript();
|
|
165
|
+
complete(await diagnoseESLint(root, { profile: 'javascript' }));
|
|
166
|
+
assert.equal(fs.existsSync(path.join(root, 'tsconfig.json')), false);
|
|
167
|
+
assert.equal(fs.existsSync(path.join(root, 'node_modules/typescript')), false);
|
|
168
|
+
write(root, 'eslint.config.mjs', 'export default [];\n');
|
|
169
|
+
incomplete(await diagnoseESLint(root, { profile: 'javascript' }), /config/i);
|
|
170
|
+
fs.unlinkSync(path.join(root, 'eslint.config.mjs'));
|
|
171
|
+
fs.unlinkSync(path.join(root, 'node_modules/eslint'));
|
|
172
|
+
incomplete(await diagnoseESLint(root, { profile: 'javascript' }), /eslint|local|ENOENT/i);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
/** @id TEST-DIAGJS-002 @verifies REQ-DIAGJS-002 */
|
|
176
|
+
test('TEST-DIAGJS-002 whole-project extensions, ignores and directory symlinks', async () => {
|
|
177
|
+
const root = javascript();
|
|
178
|
+
for (const name of ['b.jsx', 'nested/c.mjs', 'd.cjs', 'ignored.js']) write(root, name, 'debugger;\n');
|
|
179
|
+
write(root, 'not-input.ts', 'debugger;\n');
|
|
180
|
+
fs.symlinkSync(root, path.join(root, 'loop'), 'dir');
|
|
181
|
+
const value = complete(await diagnoseESLint(root, { profile: 'javascript' }));
|
|
182
|
+
assert.deepEqual(value.files, { included: 6, checked: 5, ignored: 1 });
|
|
183
|
+
assert.deepEqual(value.diagnostics.filter(d => d.severity === 'error').map(d => d.file), ['b.jsx', 'd.cjs', 'nested/c.mjs']);
|
|
184
|
+
for (const name of ['a.js', 'b.jsx', 'nested/c.mjs', 'd.cjs']) fs.unlinkSync(path.join(root, name));
|
|
185
|
+
write(root, 'eslint.config.cjs', 'module.exports=[{ignores:["**"]}];\n');
|
|
186
|
+
incomplete(await diagnoseESLint(root, { profile: 'javascript' }), /ignored|input/i);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
/** @id TEST-DIAGJS-003 @verifies REQ-DIAGJS-003 */
|
|
190
|
+
test('TEST-DIAGJS-003 severities, UTF16, parser errors and no fix/cache', async () => {
|
|
191
|
+
const root = javascript();
|
|
192
|
+
write(root, 'a.js', 'const unused = "😀"; debugger;\n');
|
|
193
|
+
const before = snapshot(root);
|
|
194
|
+
const value = complete(await diagnoseESLint(root, { profile: 'javascript' }));
|
|
195
|
+
assert.ok(value.diagnostics.some(d => d.severity === 'warning'));
|
|
196
|
+
assert.equal(value.diagnostics.find(d => d.code === 'no-debugger').column, 'const unused = "😀"; '.length + 1);
|
|
197
|
+
assert.deepEqual(snapshot(root), before);
|
|
198
|
+
write(root, 'a.js', 'const = ;\n');
|
|
199
|
+
assert.ok(complete(await diagnoseESLint(root, { profile: 'javascript' })).diagnostics.some(d => d.severity === 'error'));
|
|
200
|
+
write(root, 'eslint.config.cjs', 'throw new Error("controlled config failure");\n');
|
|
201
|
+
incomplete(await diagnoseESLint(root, { profile: 'javascript' }), /controlled config failure/);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
/** @id TEST-DIAGPY-001 @verifies REQ-DIAGPY-001 */
|
|
205
|
+
test('TEST-DIAGPY-001 exact root venv tools and independent attempts', () => {
|
|
206
|
+
const root = python();
|
|
207
|
+
const data = cli(root, 'python');
|
|
208
|
+
assert.equal(data.exitCode, 0, JSON.stringify(data));
|
|
209
|
+
assert.deepEqual(data.providers.map(p => p.source), ['ruff', 'mypy']);
|
|
210
|
+
data.providers.forEach(complete);
|
|
211
|
+
fs.unlinkSync(path.join(root, '.venv'));
|
|
212
|
+
const absent = cli(root, 'python');
|
|
213
|
+
assert.equal(absent.exitCode, 2);
|
|
214
|
+
assert.deepEqual(absent.providers.map(p => p.source), ['ruff', 'mypy']);
|
|
215
|
+
absent.providers.forEach(p => incomplete(p, /venv|Python|ENOENT/i));
|
|
216
|
+
const marker = path.join(project(), 'unsupported-entry');
|
|
217
|
+
const isolated = isolatedPython({ ruff: `from pathlib import Path\nPath(${JSON.stringify(marker)}).write_text("must not execute")\n`, version: '0.17.0' });
|
|
218
|
+
write(isolated.site, 'attack.pth', `import pathlib; pathlib.Path(${JSON.stringify(marker)}).write_text("pth executed")\n`);
|
|
219
|
+
write(isolated.root, 'a.py', 'value: int = 1\n');
|
|
220
|
+
const mixed = cli(isolated.root, 'python');
|
|
221
|
+
incomplete(mixed.providers[0], /unsupported.*ruff|version/i);
|
|
222
|
+
complete(mixed.providers[1]);
|
|
223
|
+
assert.equal(fs.existsSync(marker), false);
|
|
224
|
+
write(isolated.root, 'mypy.ini', '[mypy]\npython_executable=/not/the/selected/venv/python\n');
|
|
225
|
+
complete(provider(isolated.root, 'mypy'));
|
|
226
|
+
const oldMypy = isolatedPython({ mypy: '', mypyVersion: '2.3.0' });
|
|
227
|
+
write(oldMypy.root, 'a.py', 'value: int = 1\n');
|
|
228
|
+
write(oldMypy.site, 'mypy/__init__.py', `from pathlib import Path\nPath(${JSON.stringify(marker)}).write_text("unsupported mypy entry")\n`);
|
|
229
|
+
const other = cli(oldMypy.root, 'python');
|
|
230
|
+
complete(other.providers[0]);
|
|
231
|
+
incomplete(other.providers[1], /unsupported.*mypy|version/i);
|
|
232
|
+
assert.equal(fs.existsSync(marker), false);
|
|
233
|
+
fs.symlinkSync(toolVenv(), path.join(root, '.venv'), 'dir');
|
|
234
|
+
const bridge = path.join(here, 'diagnostics-python.py');
|
|
235
|
+
for (const [version, exit] of [[[3, 13, 0, 'final', 0], 2], [[3, 14, 0, 'candidate', 1], 2], [[3, 15, 0, 'final', 0], 2], [[3, 14, 0, 'final', 0], 0]]) {
|
|
236
|
+
const code = `import sys,json,os,runpy,pkgutil;from collections import namedtuple;sys.version_info=namedtuple("Version","major minor micro releaselevel serial")(*${JSON.stringify(version)});sys.argv=${JSON.stringify([bridge, root, 'ruff', 'probe'])};runpy.run_path(${JSON.stringify(bridge)},run_name="__main__")`;
|
|
237
|
+
const result = spawnSync(path.join(root, '.venv/bin/python'), ['-B', '-I', '-S', '-c', code], { encoding: 'utf8', cwd: root, timeout: 10000 });
|
|
238
|
+
assert.ifError(result.error);
|
|
239
|
+
assert.equal(result.status, exit, result.stdout + result.stderr);
|
|
240
|
+
const data = JSON.parse(result.stdout);
|
|
241
|
+
if (exit) assert.match(data.error, /Unsupported Python version/);
|
|
242
|
+
else assert.equal(data.version, '0.17.1');
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
/** @id TEST-DIAGPY-002 @verifies REQ-DIAGPY-002 */
|
|
247
|
+
test('TEST-DIAGPY-002 direct counts, root configs, imports and no symlink traversal', () => {
|
|
248
|
+
const root = python(), outside = project();
|
|
249
|
+
write(root, 'ruff.toml', 'exclude=["skip.py"]\n');
|
|
250
|
+
write(root, 'mypy.ini', '[mypy]\nfiles=api.pyi\nexclude=(^|/)skip\\.py$\n');
|
|
251
|
+
write(root, 'api.pyi', 'value: int\n');
|
|
252
|
+
write(root, 'a.py', 'from skip import value\n');
|
|
253
|
+
write(root, 'skip.py', 'value: int = "bad"\n');
|
|
254
|
+
write(outside, 'outside.py', 'outside_missing\n');
|
|
255
|
+
fs.symlinkSync(outside, path.join(root, 'linked'), 'dir');
|
|
256
|
+
write(root, 'notebook.ipynb', '{"cells":[],"metadata":{},"nbformat":4,"nbformat_minor":5}');
|
|
257
|
+
const data = cli(root, 'python');
|
|
258
|
+
assert.deepEqual(data.providers.map(p => p.source), ['ruff', 'mypy']);
|
|
259
|
+
for (const p of data.providers) assert.deepEqual(complete(p).files, { included: 3, checked: 2, ignored: 1 });
|
|
260
|
+
assert.ok(data.providers[1].diagnostics.some(d => d.file === 'skip.py' && d.code === 'assignment'));
|
|
261
|
+
write(root, 'line\nbreak.py', 'missing\n');
|
|
262
|
+
cli(root, 'python').providers.forEach(p => incomplete(p, /path|newline|CR|LF/i));
|
|
263
|
+
const configs = python();
|
|
264
|
+
write(configs, 'a.py', 'missing\n');
|
|
265
|
+
write(configs, '.ruff.toml', '[lint]\nselect=["F821"]\n');
|
|
266
|
+
write(configs, 'ruff.toml', '[lint]\nignore=["F821"]\n');
|
|
267
|
+
write(configs, 'mypy.ini', '[mypy]\nmodules=does_not_exist\n');
|
|
268
|
+
write(configs, '.mypy.ini', '[mypy]\npackages=does_not_exist\n');
|
|
269
|
+
write(configs, 'pyproject.toml', '[tool.ruff.lint]\nignore=["F821"]\n[tool.mypy]\nfiles=["does_not_exist"]\n');
|
|
270
|
+
write(configs, 'setup.cfg', '[mypy]\npackages=does_not_exist\n');
|
|
271
|
+
for (const [ruffConfig, mypyConfig] of [['.ruff.toml', 'mypy.ini'], ['ruff.toml', '.mypy.ini'], ['pyproject.toml', 'pyproject.toml'], [null, 'setup.cfg'], [null, null]]) {
|
|
272
|
+
const data = cli(configs, 'python');
|
|
273
|
+
for (const [index, config] of [[0, ruffConfig], [1, mypyConfig]]) {
|
|
274
|
+
assert.equal(complete(data.providers[index]).configPath, config ? path.join(configs, config) : null);
|
|
275
|
+
}
|
|
276
|
+
if (ruffConfig) fs.unlinkSync(path.join(configs, ruffConfig));
|
|
277
|
+
if (mypyConfig && mypyConfig !== ruffConfig) fs.unlinkSync(path.join(configs, mypyConfig));
|
|
278
|
+
}
|
|
279
|
+
const nested = path.join(configs, 'nested');
|
|
280
|
+
fs.mkdirSync(nested);
|
|
281
|
+
fs.symlinkSync(toolVenv(), path.join(nested, '.venv'), 'dir');
|
|
282
|
+
write(configs, 'pyproject.toml', '[tool.ruff.lint]\nignore=["F821"]\n[tool.mypy]\ndisable_error_code=["name-defined"]\n');
|
|
283
|
+
write(nested, 'a.py', 'missing\n');
|
|
284
|
+
const independent = cli(nested, 'python');
|
|
285
|
+
assert.ok(complete(independent.providers[0]).diagnostics.some(d => d.code === 'F821'));
|
|
286
|
+
assert.ok(complete(independent.providers[1]).diagnostics.some(d => d.code === 'name-defined'));
|
|
287
|
+
write(nested, 'pyproject.toml', '[tool.\n');
|
|
288
|
+
cli(nested, 'python').providers.forEach(p => incomplete(p));
|
|
289
|
+
write(nested, 'pyproject.toml', '[project]\nname="unrelated-section"\n');
|
|
290
|
+
write(nested, 'setup.cfg', '[other]\nvalue=1\n');
|
|
291
|
+
cli(nested, 'python').providers.forEach(p => assert.equal(complete(p).configPath, null));
|
|
292
|
+
fs.unlinkSync(path.join(nested, 'a.py'));
|
|
293
|
+
cli(nested, 'python').providers.forEach(p => incomplete(p));
|
|
294
|
+
write(nested, 'a.py', 'value: int = 1\n');
|
|
295
|
+
write(nested, '.ruff.toml', 'exclude=["*.py"]\n');
|
|
296
|
+
write(nested, 'mypy.ini', '[mypy]\nexclude=.*\n');
|
|
297
|
+
cli(nested, 'python').providers.forEach(p => incomplete(p));
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
/** @id TEST-DIAGPY-003 @verifies REQ-DIAGPY-003 */
|
|
301
|
+
test('TEST-DIAGPY-003 Ruff JSON, Unicode codepoints and syntax errors', async () => {
|
|
302
|
+
const root = python();
|
|
303
|
+
write(root, 'a.py', 'emoji = "😀"; missing\n');
|
|
304
|
+
const p = complete(provider(root, 'ruff'));
|
|
305
|
+
const error = p.diagnostics.find(d => d.code === 'F821');
|
|
306
|
+
assert.ok(error, JSON.stringify(p));
|
|
307
|
+
assert.equal(error.severity, 'error');
|
|
308
|
+
assert.equal(error.line, 1);
|
|
309
|
+
assert.equal(error.column, 'emoji = "😀"; '.length + 1);
|
|
310
|
+
write(root, 'a.py', 'value = ;\n');
|
|
311
|
+
assert.ok(complete(provider(root, 'ruff')).diagnostics.some(d => d.code === 'invalid-syntax'));
|
|
312
|
+
await checkPythonProtocol();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
/** @id TEST-DIAGPY-004 @verifies REQ-DIAGPY-004 */
|
|
316
|
+
test('TEST-DIAGPY-004 mypy JSONL zero/unknown positions, notes and incomplete syntax', async () => {
|
|
317
|
+
const root = python();
|
|
318
|
+
write(root, 'a.py', 'missing\n');
|
|
319
|
+
const zero = complete(provider(root, 'mypy')).diagnostics.find(d => d.code === 'name-defined');
|
|
320
|
+
assert.equal(zero.column, 1);
|
|
321
|
+
write(root, 'a.py', 'emoji = "😀"; missing; other_missing\n');
|
|
322
|
+
const unicode = complete(provider(root, 'mypy'));
|
|
323
|
+
assert.equal(unicode.diagnostics.filter(d => d.code === 'name-defined').length, 2);
|
|
324
|
+
assert.equal(unicode.diagnostics.find(d => d.code === 'name-defined').column, null);
|
|
325
|
+
assert.equal(unicode.diagnostics.filter(d => d.code === 'location-unavailable' && d.severity === 'info').length, 1);
|
|
326
|
+
write(root, 'a.py', 'value = ;\n');
|
|
327
|
+
const syntax = incomplete(provider(root, 'mypy'));
|
|
328
|
+
assert.ok(syntax.diagnostics.some(d => d.code === 'syntax'));
|
|
329
|
+
write(root, 'a.py', 'reveal_type(1)\n');
|
|
330
|
+
assert.ok(complete(provider(root, 'mypy')).diagnostics.some(d => d.severity === 'info'));
|
|
331
|
+
write(root, 'a.py', 'value: int = 1\n');
|
|
332
|
+
write(root, 'duplicate/a.py', 'value: int = 1\n');
|
|
333
|
+
const duplicate = incomplete(provider(root, 'mypy'));
|
|
334
|
+
const blocker = duplicate.diagnostics.find(d => d.severity === 'error' && /Duplicate module/.test(d.message));
|
|
335
|
+
assert.ok(blocker, JSON.stringify(duplicate));
|
|
336
|
+
assert.equal(blocker.line, null);
|
|
337
|
+
assert.equal(blocker.column, null);
|
|
338
|
+
assert.equal(blocker.code, 'mypy');
|
|
339
|
+
assert.match(blocker.message, /Common resolutions/);
|
|
340
|
+
fs.unlinkSync(path.join(root, 'duplicate/a.py'));
|
|
341
|
+
write(root, 'a.py', 'value: int = 1\n');
|
|
342
|
+
const records = [
|
|
343
|
+
{ file: null, line: -1, column: -1, code: null, severity: 'note', message: 'fileless' },
|
|
344
|
+
{ file: 'a.py', line: -1, column: -1, code: 'position', severity: 'error', message: 'unknown-line' },
|
|
345
|
+
{ file: 'a.py', line: 1, column: -1, code: 'position', severity: 'error', message: 'unknown-column' },
|
|
346
|
+
{ file: 'a.py', line: 1, column: 0, code: 'position', severity: 'error', message: 'zero-column' },
|
|
347
|
+
];
|
|
348
|
+
async function scripted(stdout, code = 1) {
|
|
349
|
+
const actual = nativeBudget();
|
|
350
|
+
return diagnosePython(root, 'mypy', { run(command, args, options) {
|
|
351
|
+
return args.at(-1) === 'tool' ? Promise.resolve({ code, signal: null, failure: null, stdout, stderr: Buffer.alloc(0) }) : actual.run(command, args, options);
|
|
352
|
+
} });
|
|
353
|
+
}
|
|
354
|
+
const mapped = complete(await scripted(Buffer.from(records.map(record => JSON.stringify(record)).join('\n') + '\n')));
|
|
355
|
+
for (const message of ['fileless', 'unknown-line']) {
|
|
356
|
+
const record = mapped.diagnostics.find(d => d.message === message);
|
|
357
|
+
assert.equal(record.line, null);
|
|
358
|
+
assert.equal(record.column, null);
|
|
359
|
+
}
|
|
360
|
+
assert.equal(mapped.diagnostics.find(d => d.message === 'unknown-column').column, null);
|
|
361
|
+
assert.equal(mapped.diagnostics.find(d => d.message === 'zero-column').column, 1);
|
|
362
|
+
const partial = incomplete(await scripted(Buffer.concat([Buffer.from(JSON.stringify(records[3]) + '\n{"broken":"'), Buffer.from([0xc3])])));
|
|
363
|
+
assert.ok(partial.diagnostics.some(d => d.message === 'zero-column'), 'Retain complete records before malformed UTF-8');
|
|
364
|
+
assert.equal(partial.diagnostics.find(d => d.message === 'zero-column').column, 1);
|
|
365
|
+
complete(await scripted(Buffer.alloc(0), 0));
|
|
366
|
+
incomplete(await scripted(Buffer.alloc(0)), /empty|diagnostic|exit 1/i);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
/** @id TEST-DIAGPY-005 @verifies REQ-DIAGPY-005 */
|
|
370
|
+
test('TEST-DIAGPY-005 no source, venv, bytecode or cache mutation', () => {
|
|
371
|
+
const root = python();
|
|
372
|
+
write(root, '.sdd/tdd.jsonl', 'historical evidence\n');
|
|
373
|
+
const before = snapshot(root), venvBefore = snapshot(toolVenv());
|
|
374
|
+
const value = cli(root, 'python');
|
|
375
|
+
assert.equal(value.exitCode, 0, JSON.stringify(value));
|
|
376
|
+
assert.deepEqual(snapshot(root), before);
|
|
377
|
+
assert.deepEqual(snapshot(toolVenv()), venvBefore);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
/** @id TEST-DIAGRUST-001 @verifies REQ-DIAGRUST-001 */
|
|
381
|
+
test('TEST-DIAGRUST-001 installed root toolchain, lock and workspace boundaries', () => {
|
|
382
|
+
const { root, env } = rust();
|
|
383
|
+
assert.equal(cli(root, 'rust', [], env).exitCode, 0);
|
|
384
|
+
fs.unlinkSync(path.join(root, 'Cargo.lock'));
|
|
385
|
+
incomplete(cli(root, 'rust', [], env).providers[0], /lock/i);
|
|
386
|
+
write(root, 'Cargo.lock', 'version = 4\n\n[[package]]\nname = "profile-fixture"\nversion = "0.0.0"\n');
|
|
387
|
+
incomplete(cli(root, 'rust', [], { ...env, CARGO_HOME: path.join(root, '.cargo') }).providers[0], /Cargo home|external|outside/i);
|
|
388
|
+
write(root, 'rust-toolchain.toml', '[toolchain]\nchannel="1.99.99"\n');
|
|
389
|
+
incomplete(cli(root, 'rust', [], env).providers[0], /installed|toolchain|version/i);
|
|
390
|
+
const fake = fakeRust();
|
|
391
|
+
incomplete(cli(fake.root, 'rust', [], { ...fake.env, FIXTURE_CARGO_VERSION: '1.98.0' }).providers[0], /version/i);
|
|
392
|
+
assert.equal(fs.readFileSync(fake.events, 'utf8').trim().split('\n').length, 1, 'Unsupported cargo must not execute metadata/check');
|
|
393
|
+
fs.unlinkSync(path.join(root, 'rust-toolchain.toml'));
|
|
394
|
+
write(root, 'Cargo.toml', fs.readFileSync(path.join(root, 'Cargo.toml'), 'utf8') + '[workspace]\nmembers=["member"]\n');
|
|
395
|
+
write(root, 'member/Cargo.toml', '[package]\nname="workspace-member"\nversion="0.0.0"\nedition="2024"\n');
|
|
396
|
+
write(root, 'member/src/lib.rs', 'pub fn value() {}\n');
|
|
397
|
+
write(root, 'Cargo.lock', fs.readFileSync(path.join(root, 'Cargo.lock'), 'utf8') + '\n[[package]]\nname="workspace-member"\nversion="0.0.0"\n');
|
|
398
|
+
incomplete(cli(root, 'rust', [], env).providers[0], /workspace|single-package/i);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
/** @id TEST-DIAGRUST-002 @verifies REQ-DIAGRUST-002 */
|
|
402
|
+
test('TEST-DIAGRUST-002 all targets, offline locked and private output overrides', () => {
|
|
403
|
+
const { root, env } = rust();
|
|
404
|
+
write(root, 'examples/demo.rs', 'fn main() { let _: i32 = "bad"; }\n');
|
|
405
|
+
const before = snapshot(root);
|
|
406
|
+
const value = cli(root, 'rust', [], env);
|
|
407
|
+
assert.equal(value.exitCode, 1, JSON.stringify(value));
|
|
408
|
+
assert.ok(complete(value.providers[0]).diagnostics.some(d => d.file === 'examples/demo.rs' && d.code === 'E0308'));
|
|
409
|
+
assert.deepEqual(snapshot(root), before);
|
|
410
|
+
write(root, 'Cargo.toml', '[package]\nname="profile-fixture"\nversion="0.0.0"\nedition="2024"\n[dependencies]\nmissing-profile-fixture-package="0.0.0"\n');
|
|
411
|
+
incomplete(cli(root, 'rust', [], env).providers[0], /offline|dependency|matching package/i);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
/** @id TEST-DIAGRUST-003 @verifies REQ-DIAGRUST-003 */
|
|
415
|
+
test('TEST-DIAGRUST-003 compiler versus runtime abort, nullable codes and UTF16', () => {
|
|
416
|
+
const { root, env } = rust();
|
|
417
|
+
const source = 'pub fn value() -> i32 { let _emoji = "😀"; "bad" }\n';
|
|
418
|
+
write(root, 'src/lib.rs', source);
|
|
419
|
+
const typed = cli(root, 'rust', [], env);
|
|
420
|
+
assert.equal(typed.exitCode, 1);
|
|
421
|
+
const diagnostic = complete(typed.providers[0]).diagnostics.find(d => d.code === 'E0308');
|
|
422
|
+
assert.equal(diagnostic.column, source.indexOf('"bad"') + 1);
|
|
423
|
+
write(root, 'src/lib.rs', 'pub fn broken( {\n');
|
|
424
|
+
assert.ok(complete(cli(root, 'rust', [], env).providers[0]).diagnostics.some(d => d.code === 'rustc'));
|
|
425
|
+
write(root, 'src/lib.rs', 'pub fn value() -> i32 { 1 }\n');
|
|
426
|
+
write(root, 'build.rs', 'fn main() { panic!("controlled runtime abort"); }\n');
|
|
427
|
+
incomplete(cli(root, 'rust', [], env).providers[0], /build|abort|compiler/i);
|
|
428
|
+
write(root, 'build.rs', 'fn main() { let _: i32 = "bad"; }\n');
|
|
429
|
+
assert.equal(cli(root, 'rust', [], env).exitCode, 1);
|
|
430
|
+
const fake = fakeRust();
|
|
431
|
+
const file = path.join(fake.root, 'src/lib.rs');
|
|
432
|
+
const span = { file_name: file, byte_start: 0, is_primary: false };
|
|
433
|
+
const primary = { ...span, byte_start: 7, is_primary: true };
|
|
434
|
+
const record = message => ({ reason: 'compiler-message', package_id: 'fixture-id', target: { src_path: file }, message });
|
|
435
|
+
const error = { level: 'error', message: 'controlled compiler error', code: null, spans: [span, primary],
|
|
436
|
+
children: [{ level: 'note', message: 'controlled child note', code: null, spans: [], children: [] }, { level: 'help', message: 'controlled child help', code: null, spans: [], children: [] }] };
|
|
437
|
+
const artifact = { reason: 'compiler-artifact', package_id: 'fixture-id', target: { src_path: file } };
|
|
438
|
+
function check(records, exit, stderr = '') {
|
|
439
|
+
return cli(fake.root, 'rust', [], { ...fake.env, FIXTURE_CARGO_MESSAGES: records.map(value => typeof value === 'string' ? value : JSON.stringify(value)).join('\n') + '\n', FIXTURE_CARGO_EXIT: String(exit), FIXTURE_CARGO_STDERR: stderr });
|
|
440
|
+
}
|
|
441
|
+
const preserved = check([record(error), { reason: 'build-finished', success: false }], 101, 'error: could not compile `profile-fixture` (lib) due to 1 previous error\n');
|
|
442
|
+
const diagnostics = complete(preserved.providers[0]).diagnostics;
|
|
443
|
+
assert.equal(diagnostics.find(d => d.severity === 'error').column, 8);
|
|
444
|
+
assert.ok(diagnostics.some(d => d.severity === 'info' && d.message === 'controlled child note'));
|
|
445
|
+
assert.ok(diagnostics.some(d => d.severity === 'hint' && d.message === 'controlled child help'));
|
|
446
|
+
assert.equal(complete(check([record({ ...error, spans: [] }), { reason: 'build-finished', success: false }], 101).providers[0]).diagnostics.find(d => d.severity === 'error').file, null);
|
|
447
|
+
const warning = record({ ...error, level: 'warning', children: [] });
|
|
448
|
+
assert.equal(check([artifact, warning, { reason: 'build-finished', success: true }], 0).exitCode, 0);
|
|
449
|
+
assert.equal(check([artifact, { reason: 'build-finished', success: true }], 0).exitCode, 0);
|
|
450
|
+
for (const [records, exit, stderr] of [
|
|
451
|
+
[[artifact], 0],
|
|
452
|
+
[[artifact, { reason: 'build-finished', success: false }], 101],
|
|
453
|
+
[[record(error), { reason: 'build-finished', success: true }], 101],
|
|
454
|
+
[[{ ...record(error), package_id: 'foreign-id' }, { reason: 'build-finished', success: false }], 101],
|
|
455
|
+
[[record(error), { ...record(error), package_id: 'foreign-id' }, { reason: 'build-finished', success: false }], 101],
|
|
456
|
+
[[artifact, { reason: 'build-finished', success: true }, warning], 0],
|
|
457
|
+
[[artifact, '{"reason":'], 0],
|
|
458
|
+
[[artifact, { reason: 'build-finished', success: true }, '{"reason":'], 0],
|
|
459
|
+
[[record(error), { reason: 'build-finished', success: false }], 101, 'error: failed to run custom build command for `dependency`\n'],
|
|
460
|
+
]) incomplete(check(records, exit, stderr).providers[0]);
|
|
461
|
+
const prefix = incomplete(check([record(error), record({ ...error, message: 'second complete error', children: [] }), '{"reason":'], 101).providers[0]);
|
|
462
|
+
assert.equal(prefix.diagnostics.filter(d => d.severity === 'error').length, 2);
|
|
463
|
+
const trace = fs.readFileSync(fake.events, 'utf8').trim().split('\n').map(line => JSON.parse(line));
|
|
464
|
+
assert.ok(trace.every(e => e.cwd === fake.root && e.autoInstall === '0' && e.rustc === path.join(fake.tools, 'rustc')));
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
async function checkPythonProtocol() {
|
|
468
|
+
const output = { filename: '', location: { row: 1, column: 1 }, code: 'F821', message: 'controlled error', severity: 'error' };
|
|
469
|
+
const fake = isolatedPython({ ruff: 'import sys,json,os,time\nif "--show-files" in sys.argv:\n print(os.path.join(os.getcwd(),"a.py"))\nelse:\n item=' + JSON.stringify(output) + '\n item["filename"]=os.path.join(os.getcwd(),"a.py")\n print(json.dumps([item]),flush=True)\n time.sleep(0.1)\n sys.stderr.write("x"*4096)\n' });
|
|
470
|
+
const p = await diagnosePython(fake.root, 'ruff', nativeBudget({ timeoutMs: 5000, outputBytes: 2048 }));
|
|
471
|
+
incomplete(p, /output.*2048.*bytes/i);
|
|
472
|
+
assert.ok(p.diagnostics.some(d => d.code === 'F821' && d.severity === 'error'));
|
|
473
|
+
assert.equal(p.diagnostics.find(d => d.code === 'F821').column, null);
|
|
474
|
+
const body = expression => 'import sys,json,os\nif "--show-files" in sys.argv:\n print(os.path.join(os.getcwd(),"a.py"))\nelse:\n item=' + JSON.stringify(output) + '\n item["filename"]=os.path.join(os.getcwd(),"a.py")\n ' + expression + '\n';
|
|
475
|
+
const file = path.join(fake.site, 'ruff/__main__.py');
|
|
476
|
+
write(fake.site, 'ruff/__main__.py', body('print(json.dumps([{**item,"severity":"warning"},{**item,"severity":"info"},{**item,"severity":"hint"}]))'));
|
|
477
|
+
const neutral = complete(await diagnosePython(fake.root, 'ruff', nativeBudget()));
|
|
478
|
+
assert.deepEqual(neutral.diagnostics.map(d => d.severity).sort(), ['hint', 'info', 'warning']);
|
|
479
|
+
assert.equal(core.aggregate([neutral], ['ruff']).exitCode, 0);
|
|
480
|
+
fs.writeFileSync(file, body('print(json.dumps([item,{**item,"severity":"fatal"}]))'));
|
|
481
|
+
const invalid = incomplete(await diagnosePython(fake.root, 'ruff', nativeBudget()), /schema/i);
|
|
482
|
+
assert.equal(invalid.diagnostics.filter(d => d.code === 'F821').length, 1);
|
|
483
|
+
fs.writeFileSync(file, body('print("not-json")'));
|
|
484
|
+
incomplete(await diagnosePython(fake.root, 'ruff', nativeBudget()), /JSON/i);
|
|
485
|
+
fs.writeFileSync(file, body('print("[]")'));
|
|
486
|
+
complete(await diagnosePython(fake.root, 'ruff', nativeBudget()));
|
|
487
|
+
fs.writeFileSync(file, body('print("[]");sys.exit(1)'));
|
|
488
|
+
incomplete(await diagnosePython(fake.root, 'ruff', nativeBudget()), /empty|diagnostic|exit 1/i);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** @id TEST-DIAGRUST-004 @verifies REQ-DIAGRUST-004 */
|
|
492
|
+
test('TEST-DIAGRUST-004 distinct metadata source roots rather than artifacts/modules', () => {
|
|
493
|
+
const { root, env } = rust();
|
|
494
|
+
write(root, 'src/main.rs', 'fn main() {}\n');
|
|
495
|
+
write(root, 'tests/integration.rs', '#[test] fn accepted() {}\n');
|
|
496
|
+
write(root, 'examples/demo.rs', 'fn main() {}\n');
|
|
497
|
+
write(root, 'src/extra.rs', 'pub const UNUSED_MODULE: i32 = 1;\n');
|
|
498
|
+
const value = cli(root, 'rust', [], env);
|
|
499
|
+
assert.equal(value.exitCode, 0, JSON.stringify(value));
|
|
500
|
+
assert.deepEqual(complete(value.providers[0]).files, { included: 4, checked: 4, ignored: 0 });
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
/** @id TEST-DIAGCLI-007 @verifies REQ-DIAGCLI-004 */
|
|
504
|
+
test('TEST-DIAGCLI-007 cumulative native stdout/stderr bytes and elapsed time', async () => {
|
|
505
|
+
const run = (budget, code) => budget.run(process.execPath, ['-e', code], { cwd: scratch });
|
|
506
|
+
const control = await run(nativeBudget({ timeoutMs: 2000, outputBytes: 16 }), 'process.stdout.write("ok")');
|
|
507
|
+
assert.equal(control.stdout.toString(), 'ok');
|
|
508
|
+
assert.equal(control.failure, null);
|
|
509
|
+
const exact = nativeBudget({ timeoutMs: 2000, outputBytes: 16 });
|
|
510
|
+
assert.equal((await run(exact, 'process.stdout.write("a".repeat(8));process.stderr.write("b".repeat(8))')).failure, null);
|
|
511
|
+
assert.match((await run(exact, 'process.stdout.write("c")')).failure, /output.*16.*bytes/i);
|
|
512
|
+
const byteMarker = path.join(project(), 'must-not-run');
|
|
513
|
+
assert.match((await run(exact, `require("fs").writeFileSync(${JSON.stringify(byteMarker)},"unexpected")`)).failure, /output.*16.*bytes/i);
|
|
514
|
+
assert.equal(fs.existsSync(byteMarker), false);
|
|
515
|
+
const bytes = nativeBudget({ timeoutMs: 2000, outputBytes: 8388608 });
|
|
516
|
+
assert.equal((await run(bytes, 'process.stderr.write("b".repeat(8388608))')).failure, null);
|
|
517
|
+
assert.match((await run(bytes, 'process.stdout.write("c")')).failure, /output.*8388608.*bytes/i);
|
|
518
|
+
let now = 0;
|
|
519
|
+
const elapsed = nativeBudget({ timeoutMs: 2000, outputBytes: 1024, clock: () => now });
|
|
520
|
+
assert.equal((await run(elapsed, 'process.stdout.write("ok")')).failure, null);
|
|
521
|
+
now = 2001;
|
|
522
|
+
const marker = path.join(project(), 'expired-budget');
|
|
523
|
+
assert.match((await run(elapsed, `require("fs").writeFileSync(${JSON.stringify(marker)},"unexpected")`)).failure, /timed out/i);
|
|
524
|
+
assert.equal(fs.existsSync(marker), false);
|
|
525
|
+
assert.match((await elapsed.run(path.join(scratch, 'missing-tool'), [], { cwd: scratch })).failure, /timed out/i);
|
|
526
|
+
const closedMarker = path.join(project(), 'completed');
|
|
527
|
+
const late = await run(nativeBudget({ timeoutMs: 2000, outputBytes: 1024, clock: () => fs.existsSync(closedMarker) ? 2001 : 0 }),
|
|
528
|
+
`require("fs").writeFileSync(${JSON.stringify(closedMarker)},"completed");process.stdout.write("late")`);
|
|
529
|
+
assert.notEqual(late.failure, null, 'A delayed close/timer must not accept an over-budget child');
|
|
530
|
+
assert.match(late.failure, /timed out/i);
|
|
531
|
+
assert.match((await run(nativeBudget({ timeoutMs: 300, outputBytes: 1024 }), 'setTimeout(()=>process.stdout.write("late"),1500)')).failure, /timed out/i);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
async function checkScratchCleanup() {
|
|
535
|
+
const { root, env } = rust();
|
|
536
|
+
const marker = path.join(project(), 'out-dir');
|
|
537
|
+
write(root, 'build.rs', `fn main() { std::fs::write(${JSON.stringify(marker)}, std::env::var("OUT_DIR").unwrap()).unwrap(); }\n`);
|
|
538
|
+
const options = { profile: 'rust', env: { ...baseEnv, ...env } };
|
|
539
|
+
complete(await core.runProvider('cargo-check', root, options));
|
|
540
|
+
function ownedScratch() {
|
|
541
|
+
const out = fs.readFileSync(marker, 'utf8');
|
|
542
|
+
let dir = out;
|
|
543
|
+
while (dir !== path.dirname(dir) && !path.basename(dir).startsWith('musubix6-diagnostics-rust-')) dir = path.dirname(dir);
|
|
544
|
+
assert.ok(path.basename(dir).startsWith('musubix6-diagnostics-rust-'), out);
|
|
545
|
+
return dir;
|
|
546
|
+
}
|
|
547
|
+
assert.equal(fs.existsSync(ownedScratch()), false);
|
|
548
|
+
write(root, 'build.rs', `fn main() { std::fs::write(${JSON.stringify(marker)}, std::env::var("OUT_DIR").unwrap()).unwrap(); std::thread::sleep(std::time::Duration::from_secs(120)); }\n`);
|
|
549
|
+
incomplete(await core.runProvider('cargo-check', root, { ...options, timeoutMs: 5000 }), /timed out/);
|
|
550
|
+
assert.equal(fs.existsSync(ownedScratch()), false);
|
|
551
|
+
write(root, 'build.rs', `fn main() { std::fs::write(${JSON.stringify(marker)}, std::env::var("OUT_DIR").unwrap()).unwrap(); }\n`);
|
|
552
|
+
write(root, 'src/lib.rs', 'pub fn value() -> i32 { "bad" }\n');
|
|
553
|
+
const failed = await core.runProvider('cargo-check', root, { ...options, removeScratch: async () => { throw new Error('controlled cleanup denied'); } });
|
|
554
|
+
incomplete(failed, /controlled cleanup denied/);
|
|
555
|
+
assert.ok(failed.diagnostics.some(d => d.code === 'E0308'));
|
|
556
|
+
const retained = ownedScratch();
|
|
557
|
+
assert.equal(fs.existsSync(retained), true);
|
|
558
|
+
fs.rmSync(retained, { recursive: true, force: true });
|
|
559
|
+
}
|
|
560
|
+
/** @id TEST-DIAGCLI-008 @verifies REQ-DIAGCLI-004 REQ-DIAGCLI-005 */
|
|
561
|
+
test('TEST-DIAGCLI-008 parent-owned scratch cleanup on success, timeout and failure', checkScratchCleanup);
|
|
562
|
+
/** @id TEST-DIAGRUST-005 @verifies REQ-DIAGRUST-005 */
|
|
563
|
+
test('TEST-DIAGRUST-005 cleanup failure preserves compiler errors and forbids success', checkScratchCleanup);
|
|
564
|
+
|
|
565
|
+
/** @id TEST-DIAGFLOW-005 @verifies REQ-DIAGFLOW-001 REQ-DIAGFLOW-002 REQ-DIAGFLOW-004 */
|
|
566
|
+
test('TEST-DIAGFLOW-005 profile docs and source/wrapper/packed/copied 0/1/2 parity', () => {
|
|
567
|
+
const skill = fs.readFileSync(path.join(here, '../SKILL.md'), 'utf8');
|
|
568
|
+
for (const profile of ['javascript', 'python', 'rust']) assert.ok(skill.includes(`--profile ${profile}`), `Missing explicit ${profile} skill command`);
|
|
569
|
+
for (const name of ['README.md', 'README-ja.md']) {
|
|
570
|
+
const text = fs.readFileSync(path.join(repo, name), 'utf8');
|
|
571
|
+
for (const fragment of ['--profile javascript', '--profile python', '--profile rust', '0.17.1', '2.4.0', '3.14', '1.99', '180', 'Cargo', 'mypy']) assert.ok(text.includes(fragment), `${name}: missing ${fragment}`);
|
|
572
|
+
}
|
|
573
|
+
const packed = project();
|
|
574
|
+
const result = spawnSync('npm', ['pack', '--ignore-scripts', '--json', '--pack-destination', packed], { cwd: repo, env: baseEnv, encoding: 'utf8', timeout: 60000 });
|
|
575
|
+
assert.equal(result.status, 0, result.stderr);
|
|
576
|
+
const packResult = JSON.parse(result.stdout);
|
|
577
|
+
const info = Array.isArray(packResult) ? packResult[0] : Object.values(packResult)[0];
|
|
578
|
+
for (const name of ['diagnostics-python.py', 'diagnostics-python.mjs', 'diagnostics-rust.mjs', 'diagnostics-native.mjs']) assert.ok(info.files.some(f => f.path.endsWith('/' + name)), `Missing packed ${name}`);
|
|
579
|
+
const untar = spawnSync('tar', ['-xzf', path.join(packed, info.filename), '-C', packed], { encoding: 'utf8' });
|
|
580
|
+
assert.equal(untar.status, 0, untar.stderr);
|
|
581
|
+
const wrapper = path.join(packed, 'package/bin/musubix6.mjs'), copied = project();
|
|
582
|
+
const init = spawnSync(process.execPath, [wrapper, 'init', copied, '--only', 'lean-sdd-tdd'], { env: baseEnv, encoding: 'utf8' });
|
|
583
|
+
assert.equal(init.status, 0, init.stdout + init.stderr);
|
|
584
|
+
const entries = [[path.join(here, 'sdd.mjs'), []], [path.join(repo, 'bin/musubix6.mjs'), ['sdd']], [wrapper, ['sdd']], [path.join(copied, '.github/skills/lean-sdd-tdd/scripts/sdd.mjs'), []]];
|
|
585
|
+
const rustFixture = rust();
|
|
586
|
+
const cases = [
|
|
587
|
+
{ profile: 'javascript', root: javascript(), change: ['a.js', 'debugger;\n'], missing: 'eslint.config.cjs', env: {} },
|
|
588
|
+
{ profile: 'python', root: python(), change: ['a.py', 'value: int = "bad"\n'], missing: '.venv', env: {} },
|
|
589
|
+
{ profile: 'rust', ...rustFixture, change: ['src/lib.rs', 'pub fn value() -> i32 { "bad" }\n'], missing: 'Cargo.lock' },
|
|
590
|
+
];
|
|
591
|
+
const semantic = value => ({ profile: value.profile, status: value.status, exitCode: value.exitCode,
|
|
592
|
+
providers: value.providers.map(({ source, status, files, diagnostics }) => ({ source, status, files,
|
|
593
|
+
diagnostics: diagnostics.filter(d => d.code !== 'native-stderr') })) });
|
|
594
|
+
for (const item of cases) {
|
|
595
|
+
for (const exit of [0, 1, 2]) {
|
|
596
|
+
if (exit === 1) write(item.root, ...item.change);
|
|
597
|
+
if (exit === 2) fs.unlinkSync(path.join(item.root, item.missing));
|
|
598
|
+
const before = snapshot(item.root);
|
|
599
|
+
let reference;
|
|
600
|
+
for (const [entry, prefix] of entries) {
|
|
601
|
+
const actual = cli(item.root, item.profile, [], item.env, entry, prefix);
|
|
602
|
+
assert.equal(actual.exitCode, exit, JSON.stringify(actual));
|
|
603
|
+
const observed = semantic(actual);
|
|
604
|
+
if (reference) assert.deepEqual(observed, reference);
|
|
605
|
+
else reference = observed;
|
|
606
|
+
}
|
|
607
|
+
assert.deepEqual(snapshot(item.root), before);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
});
|