bigpowers 2.88.16 → 2.88.18
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/.gemini/extensions/bigpowers/gemini-extension.json +1 -1
- package/.mcp.json +2 -2
- package/.pi/package.json +1 -1
- package/CHANGELOG.md +18 -0
- package/bigpowers-mcp/build/lib/git-context.js +52 -36
- package/bigpowers-mcp/build/lib/git-context.js.map +1 -1
- package/bigpowers-mcp/src/lib/git-context.ts +9 -5
- package/dashboard/src/web/client.html +15 -8
- package/dashboard/src/web/server.js +2 -1
- package/package.json +1 -1
- package/scripts/bp-read-agents.sh +7 -5
- package/scripts/lib/__pycache__/link_utils.cpython-310.pyc +0 -0
- package/scripts/lib/__pycache__/srp_engine_links.cpython-310.pyc +0 -0
- package/scripts/lib/__pycache__/srp_engine_okf.cpython-310.pyc +0 -0
- package/scripts/lib/golden-suite-gates.sh +6 -0
- package/scripts/lib/migrate-version-common.sh +15 -5
- package/scripts/lib/migrate-version-plan.sh +7 -7
- package/scripts/lib/migrate-version-run.sh +3 -0
- package/scripts/lib/srp-engine.py +1 -60
- package/scripts/lib/srp_engine_okf.py +65 -0
- package/scripts/lib/story-verify-memo.sh +56 -0
- package/scripts/lib/test-assertions.sh +6 -0
- package/scripts/mcp-smoke.ts +48 -43
- package/scripts/run-story-verify.sh +3 -50
- package/scripts/test-golden-gate-runner.sh +18 -18
- package/scripts/test-project-runtime.sh +10 -19
- package/scripts/validate-docs-base-path.sh +15 -22
- package/scripts/validate-mcp-package.sh +18 -20
- package/scripts/validate-omp-extension.sh +23 -30
- package/tests/security/agents-import.cjs +36 -0
- package/tests/security/dashboard-bind.cjs +31 -0
- package/tests/security/dashboard-render.cjs +60 -0
- package/tests/security/git-context.cjs +44 -0
- package/tests/security/migration.cjs +76 -0
|
@@ -10,14 +10,7 @@ cd "$REPO_ROOT"
|
|
|
10
10
|
|
|
11
11
|
ERRORS=0
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
echo "FAIL: $*"
|
|
15
|
-
ERRORS=$((ERRORS + 1))
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
pass() {
|
|
19
|
-
echo "ok : $*"
|
|
20
|
-
}
|
|
13
|
+
source "$REPO_ROOT/scripts/lib/test-assertions.sh"
|
|
21
14
|
|
|
22
15
|
# ── 1. Package has exactly one pi.extensions entry ─────────────────────────
|
|
23
16
|
# pi reads extension paths from the `pi` manifest key (readPiManifest). The
|
|
@@ -25,21 +18,21 @@ pass() {
|
|
|
25
18
|
echo "--- [OMP] manifest: single extension entry ---"
|
|
26
19
|
EXT_COUNT=$(node -e "const p = require('./package.json'); const exts = p.pi?.extensions; if (!exts) { console.log(0); } else { console.log(exts.length); }")
|
|
27
20
|
if [[ "$EXT_COUNT" -eq 1 ]]; then
|
|
28
|
-
|
|
21
|
+
ta_ok "pi.extensions has exactly 1 entry"
|
|
29
22
|
else
|
|
30
|
-
|
|
23
|
+
ta_err "pi.extensions has $EXT_COUNT entries (expected 1)"
|
|
31
24
|
fi
|
|
32
25
|
|
|
33
26
|
# ── 2. Extension file exists ───────────────────────────────────────────────
|
|
34
27
|
echo "--- [OMP] extension file exists ---"
|
|
35
28
|
EXT_PATH=$(node -e "const p = require('./package.json'); console.log(p.pi?.extensions?.[0] || '')")
|
|
36
29
|
if [[ -z "$EXT_PATH" ]]; then
|
|
37
|
-
|
|
30
|
+
ta_err "No pi.extensions entry found"
|
|
38
31
|
else
|
|
39
32
|
if [[ -f "$EXT_PATH" ]]; then
|
|
40
|
-
|
|
33
|
+
ta_ok "extension file '$EXT_PATH' exists"
|
|
41
34
|
else
|
|
42
|
-
|
|
35
|
+
ta_err "extension file '$EXT_PATH' does not exist"
|
|
43
36
|
fi
|
|
44
37
|
fi
|
|
45
38
|
|
|
@@ -48,32 +41,32 @@ echo "--- [OMP] separated runtime responsibilities ---"
|
|
|
48
41
|
if [[ -n "$EXT_PATH" && -f "$EXT_PATH" ]]; then
|
|
49
42
|
for sym in "registerTool" "tool_call"; do
|
|
50
43
|
if grep -q "$sym" "$EXT_PATH" 2>/dev/null; then
|
|
51
|
-
|
|
44
|
+
ta_ok "symbol '$sym' found in extension source"
|
|
52
45
|
else
|
|
53
|
-
|
|
46
|
+
ta_err "symbol '$sym' NOT found in extension source"
|
|
54
47
|
fi
|
|
55
48
|
done
|
|
56
49
|
if grep -q 'pi\.registerCommand' "$EXT_PATH" 2>/dev/null; then
|
|
57
|
-
|
|
50
|
+
ta_err "extension registers commands already supplied by Pi prompt templates"
|
|
58
51
|
else
|
|
59
|
-
|
|
52
|
+
ta_ok "extension leaves slash-command registration to Pi prompt templates"
|
|
60
53
|
fi
|
|
61
54
|
else
|
|
62
|
-
|
|
55
|
+
ta_err "Cannot scan extension symbols — file not resolved"
|
|
63
56
|
fi
|
|
64
57
|
|
|
65
58
|
PROMPT_COUNT=$(node -e "const p = require('./package.json'); console.log(p.pi?.prompts?.length || 0)")
|
|
66
59
|
if [[ "$PROMPT_COUNT" -eq 1 ]]; then
|
|
67
|
-
|
|
60
|
+
ta_ok "pi.prompts has exactly 1 canonical prompt-template entry"
|
|
68
61
|
else
|
|
69
|
-
|
|
62
|
+
ta_err "pi.prompts has $PROMPT_COUNT entries (expected 1)"
|
|
70
63
|
fi
|
|
71
64
|
|
|
72
65
|
# ── 4. Skill discovery root is skills/ ────────────────────────────────────
|
|
73
66
|
echo "--- [OMP] source skill discoverability (skills/<name>/SKILL.md) ---"
|
|
74
67
|
SKILLS_ROOT="$REPO_ROOT/skills"
|
|
75
68
|
if [[ ! -d "$SKILLS_ROOT" ]]; then
|
|
76
|
-
|
|
69
|
+
ta_err "skills/ directory not found at $SKILLS_ROOT"
|
|
77
70
|
else
|
|
78
71
|
TOTAL=0
|
|
79
72
|
MISSING=0
|
|
@@ -81,7 +74,7 @@ else
|
|
|
81
74
|
rel="${skill_md#"$SKILLS_ROOT"/}"
|
|
82
75
|
# Each should be exactly <name>/SKILL.md (one level of nesting).
|
|
83
76
|
if [[ "$rel" != */SKILL.md || "${rel%%/*}" == "" ]]; then
|
|
84
|
-
|
|
77
|
+
ta_err "unexpected SKILL.md path: $rel"
|
|
85
78
|
MISSING=$((MISSING + 1))
|
|
86
79
|
continue
|
|
87
80
|
fi
|
|
@@ -89,11 +82,11 @@ else
|
|
|
89
82
|
done < <(find "$SKILLS_ROOT" -maxdepth 2 -name SKILL.md 2>/dev/null | sort)
|
|
90
83
|
|
|
91
84
|
if [[ "$TOTAL" -eq 0 ]]; then
|
|
92
|
-
|
|
85
|
+
ta_err "no skills found under skills/"
|
|
93
86
|
elif [[ "$MISSING" -eq 0 ]]; then
|
|
94
|
-
|
|
87
|
+
ta_ok "$TOTAL source skills all at skills/<name>/SKILL.md"
|
|
95
88
|
else
|
|
96
|
-
|
|
89
|
+
ta_err "$MISSING/$TOTAL skills at unexpected paths"
|
|
97
90
|
fi
|
|
98
91
|
fi
|
|
99
92
|
|
|
@@ -101,9 +94,9 @@ fi
|
|
|
101
94
|
echo "--- [OMP] extension targets skills/ root ---"
|
|
102
95
|
if [[ -n "$EXT_PATH" && -f "$EXT_PATH" ]]; then
|
|
103
96
|
if grep -q 'join(pluginRoot(), "skills")' "$EXT_PATH" 2>/dev/null; then
|
|
104
|
-
|
|
97
|
+
ta_ok "extension calls discoverSkills with skills/ subdirectory"
|
|
105
98
|
else
|
|
106
|
-
|
|
99
|
+
ta_err "extension does not target skills/ — check discoverSkills call in $EXT_PATH"
|
|
107
100
|
fi
|
|
108
101
|
fi
|
|
109
102
|
|
|
@@ -115,15 +108,15 @@ fi
|
|
|
115
108
|
echo "--- [OMP] runtime load smoke (scripts/omp-smoke.ts) ---"
|
|
116
109
|
SMOKE="$REPO_ROOT/scripts/omp-smoke.ts"
|
|
117
110
|
if [[ ! -f "$SMOKE" ]]; then
|
|
118
|
-
|
|
111
|
+
ta_err "omp-smoke.ts not found at $SMOKE"
|
|
119
112
|
elif ! command -v node >/dev/null 2>&1; then
|
|
120
113
|
echo "warn: node unavailable — skipping runtime load smoke"
|
|
121
114
|
else
|
|
122
115
|
SMOKE_ERR=$(mktemp)
|
|
123
116
|
if node "$SMOKE" >/dev/null 2>"$SMOKE_ERR"; then
|
|
124
|
-
|
|
117
|
+
ta_ok "extension loads without calling action methods during load"
|
|
125
118
|
else
|
|
126
|
-
|
|
119
|
+
ta_err "runtime load smoke failed — $(head -3 "$SMOKE_ERR" 2>/dev/null | tr '\n' ' ')"
|
|
127
120
|
fi
|
|
128
121
|
rm -f "$SMOKE_ERR"
|
|
129
122
|
fi
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// story: e21s03
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { execFileSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'bp-agents-'));
|
|
9
|
+
const script = path.resolve(__dirname, '../../scripts/bp-read-agents.sh');
|
|
10
|
+
const commands = [
|
|
11
|
+
"echo'; printf IMPORT_RAN > import-marker; #",
|
|
12
|
+
"node -e 'console.log(\"quoted value\")'",
|
|
13
|
+
'echo $(touch substitution-marker) `touch backtick-marker` "$HOME"; echo done',
|
|
14
|
+
"printf '%s\\n' 'spaces & symbols | < >'",
|
|
15
|
+
'echo "deploy" && echo finished'
|
|
16
|
+
];
|
|
17
|
+
const names = ['PREFLIGHT', 'TEST', 'BUILD', 'LINT', 'DEPLOY'];
|
|
18
|
+
const sections = ['Preflight', 'Test', 'Build', 'Lint', 'Deploy'];
|
|
19
|
+
try {
|
|
20
|
+
for (const source of ['AGENTS.md', 'CLAUDE.md', 'CURSOR.md']) {
|
|
21
|
+
fs.writeFileSync(path.join(root, source), commands.map((c, i) =>
|
|
22
|
+
`## ${sections[i]}\n\n\`\`\`bash\n${c}\n\`\`\`\n`).join('\n'));
|
|
23
|
+
const output = execFileSync('bash', ['-c',
|
|
24
|
+
'eval "$(bash "$1")"; printf "%s\\0" "$BP_PREFLIGHT" "$BP_TEST" "$BP_BUILD" "$BP_LINT" "$BP_DEPLOY"',
|
|
25
|
+
'import-test', script], { cwd: root, encoding: 'utf8' });
|
|
26
|
+
for (const marker of ['import-marker', 'substitution-marker', 'backtick-marker']) {
|
|
27
|
+
assert(!fs.existsSync(path.join(root, marker)), `${source}: importing executed ${marker}`);
|
|
28
|
+
}
|
|
29
|
+
assert.deepStrictEqual(output.split('\0').slice(0, -1), commands, `${source}: exact values`);
|
|
30
|
+
const printed = execFileSync('bash', [script, '--print'], { cwd: root, encoding: 'utf8' });
|
|
31
|
+
for (const command of commands) assert(printed.includes(command));
|
|
32
|
+
fs.unlinkSync(path.join(root, source));
|
|
33
|
+
}
|
|
34
|
+
assert.strictEqual(execFileSync('bash', [script], { cwd: root, encoding: 'utf8' }), '');
|
|
35
|
+
console.log(`PASS: ${names.length} command exports round-trip without execution in all 3 source formats`);
|
|
36
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const vm = require('vm');
|
|
6
|
+
|
|
7
|
+
// Replace filesystem watching and Express routing at the I/O boundary, but
|
|
8
|
+
// exercise a real TCP listener with the arguments supplied by createServer.
|
|
9
|
+
const filename = path.resolve(__dirname, '../../dashboard/src/web/server.js');
|
|
10
|
+
const moduleStub = { exports: {} };
|
|
11
|
+
const sandbox = {
|
|
12
|
+
module: moduleStub, __dirname: path.dirname(filename), console, setTimeout, clearTimeout,
|
|
13
|
+
require(name) {
|
|
14
|
+
if (name === 'path') return path;
|
|
15
|
+
if (name === '../loaders/watcher') return { watch: () => ({ on() {} }) };
|
|
16
|
+
if (name.startsWith('../loaders/')) return {};
|
|
17
|
+
assert.strictEqual(name, 'express');
|
|
18
|
+
return () => ({ get() {}, listen: (...args) => http.createServer().listen(...args) });
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
vm.runInNewContext(fs.readFileSync(filename, 'utf8'), sandbox, { filename });
|
|
22
|
+
const app = moduleStub.exports.createServer('/unused', 0);
|
|
23
|
+
const server = app.listen(0, () => {
|
|
24
|
+
try {
|
|
25
|
+
assert.strictEqual(server.address().address, '127.0.0.1');
|
|
26
|
+
assert(server.address().port > 0, 'ephemeral port and callback remain supported');
|
|
27
|
+
console.log('PASS: dashboard binds only to IPv4 loopback');
|
|
28
|
+
} catch (error) { console.error(error); process.exitCode = 1; }
|
|
29
|
+
finally { server.close(); }
|
|
30
|
+
});
|
|
31
|
+
server.on('error', error => { console.error(error); process.exitCode = 1; });
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const vm = require('vm');
|
|
5
|
+
|
|
6
|
+
// Execute the shipped client and feed its public fetch/SSE inputs. Capture
|
|
7
|
+
// generated HTML at the DOM boundary; this is not a browser execution test.
|
|
8
|
+
const nodes = new Map(['pipeline', 'metrics', 'state', 'epics', 'ledger', 'status']
|
|
9
|
+
.map(id => [id, { innerHTML: '', classList: { add() {}, remove() {} } }]));
|
|
10
|
+
const events = new Map();
|
|
11
|
+
let stream;
|
|
12
|
+
const payload = '<img src=x onerror="window.__bpXss=1"> & \'quoted\'';
|
|
13
|
+
const escaped = '<img src=x onerror="window.__bpXss=1"> & 'quoted'';
|
|
14
|
+
const snapshot = {
|
|
15
|
+
state: { activeFlow: payload, activeEpic: payload, activeStory: payload,
|
|
16
|
+
gitBranch: payload, release: { version: payload } },
|
|
17
|
+
projectMetrics: { totalBcps: payload, avgBcpPerHour: 1.5 },
|
|
18
|
+
currentVelocity: { avgBcpPerHour: 2 },
|
|
19
|
+
epics: [{ id: payload, title: payload, stories: [] }],
|
|
20
|
+
cycleTimes: [{ id: payload, bcps: payload, cycleMin: payload, bcpPerHour: 3 }]
|
|
21
|
+
};
|
|
22
|
+
const sandbox = {
|
|
23
|
+
console,
|
|
24
|
+
document: {
|
|
25
|
+
getElementById: id => nodes.get(id), querySelectorAll: () => [],
|
|
26
|
+
addEventListener() {}, createElement: () => ({ getContext: () => ({}) })
|
|
27
|
+
},
|
|
28
|
+
window: { addEventListener: (name, fn) => events.set(name, fn) },
|
|
29
|
+
fetch: async () => ({ json: async () => JSON.parse(JSON.stringify(snapshot)) }),
|
|
30
|
+
EventSource: class { constructor() { stream = this; } },
|
|
31
|
+
setInterval() { throw new Error('unexpected polling fallback'); }
|
|
32
|
+
};
|
|
33
|
+
const filename = path.resolve(__dirname, '../../dashboard/src/web/client.html');
|
|
34
|
+
const html = fs.readFileSync(filename, 'utf8');
|
|
35
|
+
for (const match of html.matchAll(/<script>([\s\S]*?)<\/script>/g)) {
|
|
36
|
+
vm.runInNewContext(match[1], sandbox, { filename });
|
|
37
|
+
}
|
|
38
|
+
function assertEscaped() {
|
|
39
|
+
for (const [panel, count] of [['state', 4], ['metrics', 2], ['epics', 2], ['ledger', 3]]) {
|
|
40
|
+
const output = nodes.get(panel).innerHTML;
|
|
41
|
+
assert(!output.includes('<img'), `${panel}: project metadata became HTML`);
|
|
42
|
+
assert.strictEqual(output.split(escaped).length - 1, count, `${panel}: literal text preserved`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function main() {
|
|
46
|
+
events.get('load')();
|
|
47
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
48
|
+
assertEscaped();
|
|
49
|
+
assert(stream, 'initial fetch should connect SSE');
|
|
50
|
+
stream.onmessage({ data: JSON.stringify(snapshot) });
|
|
51
|
+
assertEscaped();
|
|
52
|
+
stream.onmessage({ data: JSON.stringify({ state: { gitBranch: 'feature/example' },
|
|
53
|
+
projectMetrics: { totalBcps: 0, avgBcpPerHour: 0 } }) });
|
|
54
|
+
assert(nodes.get('state').innerHTML.includes('feature/example'));
|
|
55
|
+
assert(nodes.get('metrics').innerHTML.includes('>0</span>'));
|
|
56
|
+
assert(nodes.get('epics').innerHTML.includes('No epics available'));
|
|
57
|
+
assert(nodes.get('ledger').innerHTML.includes('No cycle-time data available'));
|
|
58
|
+
console.log('PASS: fetch and SSE render all project metadata as escaped text');
|
|
59
|
+
}
|
|
60
|
+
main().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// story: e32s04
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { execFileSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'bp-diff-'));
|
|
9
|
+
const git = (...args) => execFileSync('git', args, { cwd: root, stdio: 'pipe' });
|
|
10
|
+
const put = (name, value) => {
|
|
11
|
+
fs.mkdirSync(path.dirname(path.join(root, name)), { recursive: true });
|
|
12
|
+
fs.writeFileSync(path.join(root, name), value);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
const { getGitContext } = await import('../../bigpowers-mcp/build/lib/git-context.js');
|
|
17
|
+
git('init', '-q');
|
|
18
|
+
const secret = 'specs/m-secret.pem';
|
|
19
|
+
const before = 'specs/a-public.md';
|
|
20
|
+
const after = 'specs/z-public.md';
|
|
21
|
+
const outside = 'private/readme.md';
|
|
22
|
+
for (const file of [secret, before, after, outside]) put(file, 'baseline\n');
|
|
23
|
+
git('add', '.');
|
|
24
|
+
git('-c', 'user.name=Test', '-c', 'user.email=test@example.invalid',
|
|
25
|
+
'commit', '-qm', 'fixture');
|
|
26
|
+
for (const allowed of [[after], [before, after], [before], []]) {
|
|
27
|
+
git('reset', '--hard', 'HEAD');
|
|
28
|
+
put(secret, 'DUMMY_SECRET_VALUE\n');
|
|
29
|
+
put(outside, 'OUT_OF_SCOPE_VALUE\n');
|
|
30
|
+
for (const file of allowed) put(file, 'PUBLIC_CHANGE\n');
|
|
31
|
+
const result = getGitContext(root, 'diff');
|
|
32
|
+
assert.deepStrictEqual(Object.keys(result.diffs).sort(), [...allowed].sort());
|
|
33
|
+
assert.deepStrictEqual(Object.values(result.changed_files).flat().sort(), [...allowed].sort());
|
|
34
|
+
assert(!JSON.stringify(result).includes('DUMMY_SECRET_VALUE'));
|
|
35
|
+
assert(!JSON.stringify(result).includes('OUT_OF_SCOPE_VALUE'));
|
|
36
|
+
for (const file of allowed) assert(result.diffs[file].includes('+PUBLIC_CHANGE'));
|
|
37
|
+
}
|
|
38
|
+
git('reset', '--hard', 'HEAD');
|
|
39
|
+
assert.deepStrictEqual(getGitContext(root, 'diff').diffs, {});
|
|
40
|
+
console.log('PASS: secret diffs excluded in first, middle, last and only positions');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
main().catch(error => { console.error(error); process.exitCode = 1; })
|
|
44
|
+
.finally(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// story: e44s03 e44s06
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'bp-migration-'));
|
|
9
|
+
const repo = path.resolve(__dirname, '../..');
|
|
10
|
+
const project = path.join(root, 'project');
|
|
11
|
+
const specs = path.join(project, 'specs');
|
|
12
|
+
const marker = path.join(root, 'executed-marker');
|
|
13
|
+
const git = (...args) => execFileSync('git', args, { cwd: project, stdio: 'pipe' });
|
|
14
|
+
function stamp(version) {
|
|
15
|
+
fs.writeFileSync(path.join(specs, 'state.yaml'),
|
|
16
|
+
`bigpowers_version: ${JSON.stringify(version)}\nactive_flow: null\n`);
|
|
17
|
+
git('add', '.');
|
|
18
|
+
git('-c', 'user.name=Test', '-c', 'user.email=test@example.invalid',
|
|
19
|
+
'commit', '--allow-empty', '-qm', 'fixture');
|
|
20
|
+
}
|
|
21
|
+
function preview(script) {
|
|
22
|
+
const before = fs.readFileSync(path.join(specs, 'state.yaml'), 'utf8');
|
|
23
|
+
const result = spawnSync('bash', [script, '--project', project, '--dry-run'],
|
|
24
|
+
{ cwd: project, encoding: 'utf8' });
|
|
25
|
+
assert(!result.error, String(result.error));
|
|
26
|
+
assert(!fs.existsSync(marker), 'dry run executed version metadata');
|
|
27
|
+
assert.strictEqual(fs.readFileSync(path.join(specs, 'state.yaml'), 'utf8'), before);
|
|
28
|
+
assert.deepStrictEqual(fs.readdirSync(specs), ['state.yaml'], 'dry run wrote files');
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
function copyFile(relative, destination) {
|
|
32
|
+
const target = path.join(destination, relative);
|
|
33
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
34
|
+
fs.copyFileSync(path.join(repo, relative), target);
|
|
35
|
+
}
|
|
36
|
+
function installFixture(destination) {
|
|
37
|
+
for (const relative of ['package.json', 'scripts/migrate-version.sh',
|
|
38
|
+
'scripts/lib/python-env.sh', ...['common', 'plan', 'run', 'post', 'execute', 'transforms']
|
|
39
|
+
.map(name => `scripts/lib/migrate-version-${name}.sh`)]) copyFile(relative, destination);
|
|
40
|
+
const migrations = path.join(destination, 'specs/migrations');
|
|
41
|
+
fs.mkdirSync(migrations, { recursive: true });
|
|
42
|
+
fs.writeFileSync(path.join(migrations, 'registry.okf.md'),
|
|
43
|
+
'---\nmigrations:\n - id: test\n since_version: "2.80.0"\n order: 1\n---\n');
|
|
44
|
+
fs.writeFileSync(path.join(migrations, 'test.okf.md'),
|
|
45
|
+
'---\ntransforms: []\nactions_needed: []\n---\n');
|
|
46
|
+
return path.join(destination, 'scripts/migrate-version.sh');
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
fs.mkdirSync(specs, { recursive: true });
|
|
50
|
+
git('init', '-q', '-b', 'main');
|
|
51
|
+
const script = installFixture(path.join(root, 'installation'));
|
|
52
|
+
stamp(`0.0.0'; open('${marker}', 'w').write('DUMMY'); #`);
|
|
53
|
+
const hostile = preview(script);
|
|
54
|
+
assert.notStrictEqual(hostile.status, 0, 'invalid version must fail closed');
|
|
55
|
+
assert(/invalid.*version/i.test(hostile.stderr), hostile.stderr);
|
|
56
|
+
for (const version of ['1.0.0', '2.0.0', '2.20.0', '2.20.0-rc.1+build.2']) {
|
|
57
|
+
stamp(version);
|
|
58
|
+
const result = preview(script);
|
|
59
|
+
assert.strictEqual(result.status, 0, result.stderr);
|
|
60
|
+
assert(result.stdout.includes(`Detected version: ${version}`), result.stdout);
|
|
61
|
+
}
|
|
62
|
+
// The same data boundary applies to legitimate installation/project paths.
|
|
63
|
+
const installation = path.join(root, "install's directory");
|
|
64
|
+
const quotedScript = installFixture(installation);
|
|
65
|
+
stamp('1.0.0');
|
|
66
|
+
const quotedProject = path.join(root, "project's directory");
|
|
67
|
+
fs.symlinkSync(project, quotedProject, 'dir');
|
|
68
|
+
const result = spawnSync('bash', [quotedScript,
|
|
69
|
+
'--project', quotedProject, '--dry-run'], { encoding: 'utf8' });
|
|
70
|
+
assert.strictEqual(result.status, 0, result.stderr);
|
|
71
|
+
assert(result.stdout.includes('Detected version: 1.0.0'), result.stdout);
|
|
72
|
+
assert(result.stdout.includes('Transforms preview:'), result.stdout);
|
|
73
|
+
assert(result.stdout.includes('-- test: test --'), result.stdout);
|
|
74
|
+
assert(!result.stdout.includes('installed=unknown'), result.stdout);
|
|
75
|
+
console.log('PASS: migration preview rejects executable stamps and supports quoted paths');
|
|
76
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|