harness-everything 0.31.2 → 0.32.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +30 -0
- package/bin/cli.js +1 -0
- package/ci/lib/isolated-test-process.js +30 -0
- package/ci/mechanism-2al-codex-hooks-compat.test.js +14 -14
- package/ci/mechanism-2ao-plugin-sync.test.js +19 -2
- package/ci/mechanism-2at-fable-execution-phases.test.js +46 -0
- package/ci/mechanism-2au-codex-runtime-safety.test.js +340 -0
- package/ci/mechanism-2av-test-home-isolation.test.js +48 -0
- package/ci/mechanism-2az-step-type-registry.test.js +837 -0
- package/ci/mechanism-2ba-step-type-docs.test.js +166 -0
- package/ci/mechanism-test.js +2 -2
- package/docs/platform-capabilities.md +4 -0
- package/docs/platform-compatibility.json +1 -1
- package/fable-mode/SKILL.md +1 -1
- package/fable-mode/execution-guardrails/SKILL.md +1 -1
- package/fable-mode/fable-haiku/SKILL.md +1 -1
- package/fable-mode/fable-opus/SKILL.md +1 -1
- package/fable-mode/fable-sonnet/SKILL.md +1 -1
- package/fable-mode/references/execution-phases.md +93 -25
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/references/router-workflow-plan.md +18 -0
- package/harness-everything/scripts/kernel-router.js +37 -5
- package/hooks/scripts/codex-user-prompt.js +5 -2
- package/hooks/scripts/lib/step-types.json +37 -0
- package/hooks/scripts/lib/workflow-obligations.js +117 -14
- package/hooks/scripts/workflow-disposition.js +16 -9
- package/hooks/scripts/workflow-stop-gate.js +31 -8
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +2 -2
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/scripts/codex-user-prompt.js +5 -2
- package/plugins/harness-everything/hooks/scripts/lib/step-types.json +37 -0
- package/plugins/harness-everything/hooks/scripts/lib/workflow-obligations.js +117 -14
- package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +16 -9
- package/plugins/harness-everything/hooks/scripts/workflow-stop-gate.js +31 -8
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/fable-mode/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/execution-guardrails/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-haiku/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-opus/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-sonnet/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/references/execution-phases.md +93 -25
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/references/router-workflow-plan.md +18 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +37 -5
- package/plugins/harness-everything/skills/self-evolve/SKILL.md +1 -1
- package/plugins/harness-everything/skills/self-evolve/scripts/self-regression.js +8 -6
- package/sbom.cdx.json +7 -7
- package/scripts/codex-hooks-compat.js +265 -25
- package/self-evolve/SKILL.md +1 -1
- package/self-evolve/scripts/self-regression.js +8 -6
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
8
|
+
const compat = require('../scripts/codex-hooks-compat');
|
|
9
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-323-'));
|
|
10
|
+
let failures = 0;
|
|
11
|
+
function test(name, run) {
|
|
12
|
+
try { run(); console.log('PASS ' + name); }
|
|
13
|
+
catch (error) { failures++; console.error('FAIL ' + name + ': ' + error.message); }
|
|
14
|
+
}
|
|
15
|
+
// Git for Windows rejects the NUL device as a config file; use real empty paths.
|
|
16
|
+
const gitConfig = path.join(root, 'empty.gitconfig');
|
|
17
|
+
const gitHooks = path.join(root, 'no-git-hooks');
|
|
18
|
+
fs.writeFileSync(gitConfig, '');
|
|
19
|
+
fs.mkdirSync(gitHooks);
|
|
20
|
+
function samePathOf(left, right) {
|
|
21
|
+
const normalize = value => path.resolve(value).toLowerCase();
|
|
22
|
+
return Boolean(left && right) && normalize(left) === normalize(right);
|
|
23
|
+
}
|
|
24
|
+
function git(cwd, args) {
|
|
25
|
+
const result = spawnSync('git', ['-c', 'core.hooksPath=' + gitHooks, ...args], {
|
|
26
|
+
cwd, encoding: 'utf8', env: { ...process.env, GIT_CONFIG_GLOBAL: gitConfig, GIT_CONFIG_NOSYSTEM: '1' },
|
|
27
|
+
});
|
|
28
|
+
assert.strictEqual(result.status, 0, result.stderr);
|
|
29
|
+
return result.stdout.trim();
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
test('A survives B; C collects A; repeat C retains B; uninstall removes retained runtimes', () => {
|
|
33
|
+
const source = path.join(root, 'cache', 'plugin');
|
|
34
|
+
fs.cpSync(compat.SOURCE_PLUGIN_ROOT, source, { recursive: true });
|
|
35
|
+
const codexHome = path.join(root, 'retention-home');
|
|
36
|
+
const install = () => compat.install({ codexHome, sourcePluginRoot: source });
|
|
37
|
+
const a = install();
|
|
38
|
+
fs.writeFileSync(path.join(source, 'revision.txt'), 'B');
|
|
39
|
+
const b = install();
|
|
40
|
+
const oldScript = path.join(a.runtimeDir, 'hooks', 'scripts', 'codex-action-gate-pre.js');
|
|
41
|
+
assert(fs.existsSync(oldScript), 'A hook script must still resolve after B');
|
|
42
|
+
// Resolve the retained script without executing a hook against any user state.
|
|
43
|
+
assert.strictEqual(require.resolve(oldScript), fs.realpathSync(oldScript));
|
|
44
|
+
fs.writeFileSync(path.join(source, 'revision.txt'), 'C');
|
|
45
|
+
const c = install();
|
|
46
|
+
assert(!fs.existsSync(a.runtimeDir), 'third distinct install must collect A');
|
|
47
|
+
assert(fs.existsSync(b.runtimeDir));
|
|
48
|
+
install();
|
|
49
|
+
assert(fs.existsSync(b.runtimeDir), 'same-fingerprint update must retain B');
|
|
50
|
+
assert.deepStrictEqual(fs.readdirSync(path.dirname(c.runtimeDir)).sort(),
|
|
51
|
+
[path.basename(b.runtimeDir), path.basename(c.runtimeDir)].sort());
|
|
52
|
+
compat.uninstall({ codexHome });
|
|
53
|
+
assert(!fs.existsSync(path.dirname(c.runtimeDir)), 'uninstall removes every retained runtime');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('status reports a lost previous runtime; same-fingerprint reinstall resets it', () => {
|
|
57
|
+
const source = path.join(root, 'cache', 'plugin-health');
|
|
58
|
+
fs.cpSync(compat.SOURCE_PLUGIN_ROOT, source, { recursive: true });
|
|
59
|
+
const codexHome = path.join(root, 'health-home');
|
|
60
|
+
const install = () => compat.install({ codexHome, sourcePluginRoot: source });
|
|
61
|
+
const a = install();
|
|
62
|
+
fs.writeFileSync(path.join(source, 'revision.txt'), 'B');
|
|
63
|
+
const b = install();
|
|
64
|
+
let report = compat.status({ codexHome });
|
|
65
|
+
assert.strictEqual(report.healthy, true);
|
|
66
|
+
assert.strictEqual(report.runtimes.previous.status, 'present');
|
|
67
|
+
assert(report.runtimes.previous.runtimeDir === a.runtimeDir);
|
|
68
|
+
fs.writeFileSync(path.join(a.runtimeDir, 'tampered.txt'), 'x');
|
|
69
|
+
report = compat.status({ codexHome });
|
|
70
|
+
assert.strictEqual(report.healthy, false);
|
|
71
|
+
assert.strictEqual(report.runtimes.previous.status, 'modified');
|
|
72
|
+
fs.rmSync(a.runtimeDir, { recursive: true, force: true });
|
|
73
|
+
report = compat.status({ codexHome });
|
|
74
|
+
assert.strictEqual(report.healthy, false, 'missing previous runtime must not report healthy');
|
|
75
|
+
assert.strictEqual(report.runtimes.current.healthy, true);
|
|
76
|
+
assert.strictEqual(report.runtimes.previous.status, 'missing');
|
|
77
|
+
assert.strictEqual(report.observationCapture.status, 'configured-unverified');
|
|
78
|
+
assert.strictEqual(report.observationCapture.reason, 'previous-runtime-missing');
|
|
79
|
+
const cli = spawnSync(process.execPath, [path.resolve(__dirname, '../scripts/codex-hooks-compat.js'),
|
|
80
|
+
'status', '--codex-home', codexHome], { encoding: 'utf8' });
|
|
81
|
+
assert.strictEqual(cli.status, 1, 'CLI status must fail when the previous runtime is lost');
|
|
82
|
+
const again = install();
|
|
83
|
+
assert.strictEqual(again.runtimeDir, b.runtimeDir);
|
|
84
|
+
report = compat.status({ codexHome });
|
|
85
|
+
assert.strictEqual(report.healthy, true);
|
|
86
|
+
assert.strictEqual(report.runtimes.previous.status, 'none');
|
|
87
|
+
compat.uninstall({ codexHome });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('overlapping installs serialize; neither collects the other current runtime', () => {
|
|
91
|
+
const codexHome = path.join(root, 'concurrent-home');
|
|
92
|
+
const base = path.join(root, 'cache', 'plugin-concurrent');
|
|
93
|
+
fs.cpSync(compat.SOURCE_PLUGIN_ROOT, base, { recursive: true });
|
|
94
|
+
const a = compat.install({ codexHome, sourcePluginRoot: base });
|
|
95
|
+
const sources = {};
|
|
96
|
+
for (const id of ['B', 'C']) {
|
|
97
|
+
sources[id] = path.join(root, 'cache', 'plugin-concurrent-' + id);
|
|
98
|
+
fs.cpSync(base, sources[id], { recursive: true });
|
|
99
|
+
fs.writeFileSync(path.join(sources[id], 'revision.txt'), id);
|
|
100
|
+
}
|
|
101
|
+
const barrier = path.join(root, 'barrier');
|
|
102
|
+
fs.mkdirSync(barrier);
|
|
103
|
+
// Each child waits after preparing its runtime until the peer has also prepared one,
|
|
104
|
+
// forcing the reviewer's interleaving. A serialized peer never arrives, so the wait
|
|
105
|
+
// is bounded and the first holder proceeds alone.
|
|
106
|
+
const child = path.join(root, 'concurrent-child.js');
|
|
107
|
+
fs.writeFileSync(child, `
|
|
108
|
+
const fs = require('fs');
|
|
109
|
+
const path = require('path');
|
|
110
|
+
const [compatPath, codexHome, source, barrier, id, peer] = process.argv.slice(2);
|
|
111
|
+
const compat = require(compatPath);
|
|
112
|
+
const result = compat.install({ codexHome, sourcePluginRoot: source, onRuntimeReady() {
|
|
113
|
+
fs.writeFileSync(path.join(barrier, id), '');
|
|
114
|
+
const deadline = Date.now() + 1500;
|
|
115
|
+
while (!fs.existsSync(path.join(barrier, peer)) && Date.now() < deadline) {
|
|
116
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
|
|
117
|
+
}
|
|
118
|
+
} });
|
|
119
|
+
process.stdout.write(JSON.stringify(result));
|
|
120
|
+
`);
|
|
121
|
+
const driver = path.join(root, 'concurrent-driver.js');
|
|
122
|
+
fs.writeFileSync(driver, `
|
|
123
|
+
const { spawn } = require('child_process');
|
|
124
|
+
const [child, compatPath, codexHome, barrier, sourceB, sourceC] = process.argv.slice(2);
|
|
125
|
+
const runs = [['B', 'C', sourceB], ['C', 'B', sourceC]].map(([id, peer, source]) => new Promise(resolve => {
|
|
126
|
+
const proc = spawn(process.execPath, [child, compatPath, codexHome, source, barrier, id, peer]);
|
|
127
|
+
let out = '', err = '';
|
|
128
|
+
proc.stdout.on('data', chunk => { out += chunk; });
|
|
129
|
+
proc.stderr.on('data', chunk => { err += chunk; });
|
|
130
|
+
proc.on('close', code => resolve({ id, code, out, err }));
|
|
131
|
+
}));
|
|
132
|
+
Promise.all(runs).then(results => process.stdout.write(JSON.stringify(results)));
|
|
133
|
+
`);
|
|
134
|
+
const compatPath = path.resolve(__dirname, '../scripts/codex-hooks-compat.js');
|
|
135
|
+
const run = spawnSync(process.execPath, [driver, child, compatPath, codexHome, barrier, sources.B, sources.C],
|
|
136
|
+
{ encoding: 'utf8', timeout: 120000 });
|
|
137
|
+
assert.strictEqual(run.status, 0, run.stderr);
|
|
138
|
+
const results = JSON.parse(run.stdout);
|
|
139
|
+
for (const result of results) assert.strictEqual(result.code, 0, result.id + ': ' + result.err);
|
|
140
|
+
const installed = Object.fromEntries(results.map(result => [result.id, JSON.parse(result.out).runtimeDir]));
|
|
141
|
+
|
|
142
|
+
const manifest = JSON.parse(fs.readFileSync(compat.pathsFor(codexHome).manifestFile, 'utf8'));
|
|
143
|
+
assert(fs.existsSync(manifest.runtimeDir), 'final current runtime must exist');
|
|
144
|
+
assert.strictEqual(compat.treeHash(manifest.runtimeDir), manifest.sourcePluginSha256);
|
|
145
|
+
const other = samePathOf(manifest.runtimeDir, installed.B) ? installed.C : installed.B;
|
|
146
|
+
assert(samePathOf(manifest.previousRuntimeDir, other), 'serialized peer becomes the previous runtime');
|
|
147
|
+
assert(fs.existsSync(other), 'no installer may collect the peer runtime it just committed');
|
|
148
|
+
assert(!fs.existsSync(a.runtimeDir), 'only the oldest runtime is collected');
|
|
149
|
+
const hooks = fs.readFileSync(path.join(codexHome, 'hooks.json'), 'utf8').replace(/\\\\/g, '/');
|
|
150
|
+
const runtime = manifest.runtimeDir.replace(/\\/g, '/');
|
|
151
|
+
const referenced = hooks.match(new RegExp(runtime.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '/[^"\\s]+\\.js', 'g')) || [];
|
|
152
|
+
assert(referenced.length > 0, 'hooks.json must reference the current runtime');
|
|
153
|
+
for (const file of referenced) assert(fs.existsSync(file), 'configured command must resolve: ' + file);
|
|
154
|
+
assert.strictEqual(compat.status({ codexHome }).healthy, true);
|
|
155
|
+
assert(!fs.existsSync(compat.pathsFor(codexHome).ownerRoot + '.lock'), 'lock must be released');
|
|
156
|
+
compat.uninstall({ codexHome });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('stale lock from a dead process is broken; a live lock times out before writes', () => {
|
|
160
|
+
const source = path.join(root, 'cache', 'plugin-lock');
|
|
161
|
+
fs.cpSync(compat.SOURCE_PLUGIN_ROOT, source, { recursive: true });
|
|
162
|
+
const codexHome = path.join(root, 'lock-home');
|
|
163
|
+
const lockDir = compat.pathsFor(codexHome).ownerRoot + '.lock';
|
|
164
|
+
const dead = spawnSync(process.execPath, ['-e', 'process.stdout.write(String(process.pid))'], { encoding: 'utf8' });
|
|
165
|
+
for (const dir of [lockDir, lockDir + '.guard']) {
|
|
166
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
167
|
+
fs.writeFileSync(path.join(dir, 'owner.json'),
|
|
168
|
+
JSON.stringify({ pid: Number(dead.stdout), hostname: os.hostname(), token: 'crashed' }));
|
|
169
|
+
}
|
|
170
|
+
compat.install({ codexHome, sourcePluginRoot: source, lockTimeoutMs: 5000 });
|
|
171
|
+
assert(!fs.existsSync(lockDir), 'stale lock must be replaced and released');
|
|
172
|
+
assert(!fs.existsSync(lockDir + '.guard'), 'guard left by a crashed process must be reclaimed');
|
|
173
|
+
|
|
174
|
+
fs.mkdirSync(lockDir);
|
|
175
|
+
fs.writeFileSync(path.join(lockDir, 'owner.json'), JSON.stringify({ pid: process.pid, hostname: os.hostname() }));
|
|
176
|
+
const before = fs.readFileSync(path.join(codexHome, 'hooks.json'));
|
|
177
|
+
fs.writeFileSync(path.join(source, 'revision.txt'), 'locked');
|
|
178
|
+
assert.throws(() => compat.install({ codexHome, sourcePluginRoot: source, lockTimeoutMs: 200 }), /timed out.*lock/i);
|
|
179
|
+
assert.throws(() => compat.uninstall({ codexHome, lockTimeoutMs: 200 }), /timed out.*lock/i);
|
|
180
|
+
assert.deepStrictEqual(fs.readFileSync(path.join(codexHome, 'hooks.json')), before);
|
|
181
|
+
assert.strictEqual(fs.readdirSync(compat.pathsFor(codexHome).runtimeRoot).length, 1, 'no runtime created while locked');
|
|
182
|
+
fs.rmSync(lockDir, { recursive: true });
|
|
183
|
+
compat.uninstall({ codexHome });
|
|
184
|
+
assert(!fs.existsSync(path.join(codexHome, 'harness-everything')), 'uninstall leaves no owner or lock directories');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('three-contender stale-lock recovery admits at most one holder', () => {
|
|
188
|
+
const codexHome = path.join(root, 'stale-race-home');
|
|
189
|
+
const base = path.join(root, 'cache', 'plugin-stale-race');
|
|
190
|
+
fs.cpSync(compat.SOURCE_PLUGIN_ROOT, base, { recursive: true });
|
|
191
|
+
compat.install({ codexHome, sourcePluginRoot: base });
|
|
192
|
+
const sources = {};
|
|
193
|
+
for (const id of ['A', 'B', 'C']) {
|
|
194
|
+
sources[id] = path.join(root, 'cache', 'plugin-stale-race-' + id);
|
|
195
|
+
fs.cpSync(base, sources[id], { recursive: true });
|
|
196
|
+
fs.writeFileSync(path.join(sources[id], 'revision.txt'), id);
|
|
197
|
+
}
|
|
198
|
+
// Leave a lock owned by a dead local process.
|
|
199
|
+
const lockDir = compat.pathsFor(codexHome).ownerRoot + '.lock';
|
|
200
|
+
const dead = spawnSync(process.execPath, ['-e', 'process.stdout.write(String(process.pid))'], { encoding: 'utf8' });
|
|
201
|
+
fs.mkdirSync(lockDir);
|
|
202
|
+
fs.writeFileSync(path.join(lockDir, 'owner.json'),
|
|
203
|
+
JSON.stringify({ pid: Number(dead.stdout), hostname: os.hostname(), token: 'dead' }));
|
|
204
|
+
const barrier = path.join(root, 'stale-barrier');
|
|
205
|
+
fs.mkdirSync(barrier);
|
|
206
|
+
// Reviewer interleaving: A and B both observe the stale owner; A reclaims and holds;
|
|
207
|
+
// B then reclaims late and pauses after its rename, so C can take the vacated path.
|
|
208
|
+
// Each holder claims an exclusive marker for 1.5 s inside its critical section.
|
|
209
|
+
const child = path.join(root, 'stale-race-child.js');
|
|
210
|
+
fs.writeFileSync(child, `
|
|
211
|
+
const fs = require('fs');
|
|
212
|
+
const path = require('path');
|
|
213
|
+
const [compatPath, codexHome, source, barrier, id] = process.argv.slice(2);
|
|
214
|
+
const compat = require(compatPath);
|
|
215
|
+
const sleep = ms => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
216
|
+
const waitFor = (names, ms) => {
|
|
217
|
+
const deadline = Date.now() + ms;
|
|
218
|
+
while (!names.every(name => fs.existsSync(path.join(barrier, name))) && Date.now() < deadline) sleep(10);
|
|
219
|
+
};
|
|
220
|
+
if (id === 'C') waitFor(['A-stale', 'B-stale'], 3000);
|
|
221
|
+
let overlap = false;
|
|
222
|
+
const result = compat.install({
|
|
223
|
+
codexHome, sourcePluginRoot: source,
|
|
224
|
+
onLockEvent(event) {
|
|
225
|
+
if (event === 'stale-observed' && id !== 'C') {
|
|
226
|
+
fs.writeFileSync(path.join(barrier, id + '-stale'), '');
|
|
227
|
+
waitFor(['A-stale', 'B-stale'], 1500);
|
|
228
|
+
if (id === 'B') sleep(400);
|
|
229
|
+
}
|
|
230
|
+
if (event === 'reclaim-renamed' && id === 'B') sleep(800);
|
|
231
|
+
},
|
|
232
|
+
onRuntimeReady() {
|
|
233
|
+
const marker = path.join(barrier, 'holder');
|
|
234
|
+
try { fs.writeFileSync(marker, id, { flag: 'wx' }); }
|
|
235
|
+
catch (_) { overlap = true; return; }
|
|
236
|
+
sleep(1500);
|
|
237
|
+
fs.rmSync(marker);
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
if (overlap) { process.stderr.write(id + ' entered the critical section concurrently'); process.exit(3); }
|
|
241
|
+
process.stdout.write(JSON.stringify(result));
|
|
242
|
+
`);
|
|
243
|
+
const driver = path.join(root, 'stale-race-driver.js');
|
|
244
|
+
fs.writeFileSync(driver, `
|
|
245
|
+
const { spawn } = require('child_process');
|
|
246
|
+
const [child, compatPath, codexHome, barrier, ...sources] = process.argv.slice(2);
|
|
247
|
+
const runs = ['A', 'B', 'C'].map((id, index) => new Promise(resolve => {
|
|
248
|
+
const proc = spawn(process.execPath, [child, compatPath, codexHome, sources[index], barrier, id]);
|
|
249
|
+
let out = '', err = '';
|
|
250
|
+
proc.stdout.on('data', chunk => { out += chunk; });
|
|
251
|
+
proc.stderr.on('data', chunk => { err += chunk; });
|
|
252
|
+
proc.on('close', code => resolve({ id, code, out, err }));
|
|
253
|
+
}));
|
|
254
|
+
Promise.all(runs).then(results => process.stdout.write(JSON.stringify(results)));
|
|
255
|
+
`);
|
|
256
|
+
const compatPath = path.resolve(__dirname, '../scripts/codex-hooks-compat.js');
|
|
257
|
+
const run = spawnSync(process.execPath,
|
|
258
|
+
[driver, child, compatPath, codexHome, barrier, sources.A, sources.B, sources.C],
|
|
259
|
+
{ encoding: 'utf8', timeout: 180000 });
|
|
260
|
+
assert.strictEqual(run.status, 0, run.stderr);
|
|
261
|
+
const failed = JSON.parse(run.stdout).filter(result => result.code !== 0)
|
|
262
|
+
.map(result => result.id + ' exit ' + result.code + ': ' +
|
|
263
|
+
(result.err.split('\n').find(line => /Error|concurrently/.test(line)) || result.err.trim()));
|
|
264
|
+
assert.deepStrictEqual(failed, [], 'every contender must succeed exclusively');
|
|
265
|
+
const manifest = JSON.parse(fs.readFileSync(compat.pathsFor(codexHome).manifestFile, 'utf8'));
|
|
266
|
+
assert(fs.existsSync(manifest.runtimeDir), 'final current runtime must exist');
|
|
267
|
+
assert.strictEqual(compat.treeHash(manifest.runtimeDir), manifest.sourcePluginSha256);
|
|
268
|
+
assert(manifest.previousRuntimeDir && fs.existsSync(manifest.previousRuntimeDir), 'previous runtime must exist');
|
|
269
|
+
assert.strictEqual(compat.status({ codexHome }).healthy, true);
|
|
270
|
+
assert(!fs.existsSync(lockDir), 'lock must be released');
|
|
271
|
+
const leftovers = fs.readdirSync(path.dirname(lockDir)).filter(name => name !== 'compat-hooks');
|
|
272
|
+
assert.deepStrictEqual(leftovers, [], 'no stale tombs or guards may remain');
|
|
273
|
+
compat.uninstall({ codexHome });
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const repo = path.join(root, 'repo');
|
|
277
|
+
const source = path.join(repo, 'plugins', 'harness-everything');
|
|
278
|
+
fs.cpSync(compat.SOURCE_PLUGIN_ROOT, source, { recursive: true });
|
|
279
|
+
git(repo, ['init']);
|
|
280
|
+
git(repo, ['add', '.']);
|
|
281
|
+
git(repo, ['-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.invalid', 'commit', '-m', 'fixture']);
|
|
282
|
+
test('tracked plugin in ordinary checkout allows unrelated untracked files', () => {
|
|
283
|
+
fs.writeFileSync(path.join(repo, 'unrelated.txt'), 'unrelated');
|
|
284
|
+
compat.install({ codexHome: path.join(root, 'unrelated-home'), sourcePluginRoot: source });
|
|
285
|
+
});
|
|
286
|
+
test('packaged source inside unrelated dirty repository is allowed', () => {
|
|
287
|
+
const packaged = path.join(repo, 'node_modules', 'harness', 'plugins', 'harness-everything');
|
|
288
|
+
fs.cpSync(compat.SOURCE_PLUGIN_ROOT, packaged, { recursive: true });
|
|
289
|
+
compat.install({ codexHome: path.join(root, 'packaged-home'), sourcePluginRoot: packaged });
|
|
290
|
+
});
|
|
291
|
+
test('ignored files inside plugin subtree are excluded from the runtime, not refused', () => {
|
|
292
|
+
fs.writeFileSync(path.join(repo, '.gitignore'), '*.ignored\n.DS_Store\n');
|
|
293
|
+
git(repo, ['add', '.gitignore']);
|
|
294
|
+
git(repo, ['-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.invalid', 'commit', '-m', 'ignore']);
|
|
295
|
+
const codexHome = path.join(root, 'ignored-home');
|
|
296
|
+
const baseline = compat.install({ codexHome, sourcePluginRoot: source });
|
|
297
|
+
const ignored = [path.join(source, 'hooks', 'scripts', 'payload.ignored'), path.join(source, '.DS_Store')];
|
|
298
|
+
for (const file of ignored) fs.writeFileSync(file, 'ignored content');
|
|
299
|
+
try {
|
|
300
|
+
const result = compat.install({ codexHome, sourcePluginRoot: source });
|
|
301
|
+
assert.strictEqual(result.runtimeDir, baseline.runtimeDir, 'ignored files must not change the fingerprint');
|
|
302
|
+
assert(!fs.existsSync(path.join(result.runtimeDir, 'hooks', 'scripts', 'payload.ignored')));
|
|
303
|
+
assert(!fs.existsSync(path.join(result.runtimeDir, '.DS_Store')));
|
|
304
|
+
assert.strictEqual(compat.status({ codexHome }).healthy, true);
|
|
305
|
+
} finally {
|
|
306
|
+
for (const file of ignored) fs.rmSync(file);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
test('clean ordinary checkout installs; dirty checkout refuses before writes; explicit opt-in works', () => {
|
|
310
|
+
const codexHome = path.join(root, 'dirty-home');
|
|
311
|
+
compat.install({ codexHome, sourcePluginRoot: source });
|
|
312
|
+
fs.writeFileSync(path.join(source, 'unreleased.txt'), 'dirty');
|
|
313
|
+
const before = fs.readFileSync(path.join(codexHome, 'hooks.json'));
|
|
314
|
+
assert.throws(() => compat.install({ codexHome, sourcePluginRoot: source }), /unreleased|dirty/i);
|
|
315
|
+
assert.deepStrictEqual(fs.readFileSync(path.join(codexHome, 'hooks.json')), before);
|
|
316
|
+
compat.install({ codexHome, sourcePluginRoot: source, allowUnreleasedSource: true });
|
|
317
|
+
fs.rmSync(path.join(source, 'unreleased.txt'));
|
|
318
|
+
});
|
|
319
|
+
test('linked worktree refuses even when clean; explicit opt-in works', () => {
|
|
320
|
+
const linked = path.join(root, 'linked');
|
|
321
|
+
git(repo, ['worktree', 'add', '-b', 'fixture-linked', linked]);
|
|
322
|
+
const codexHome = path.join(root, 'linked-home');
|
|
323
|
+
const sourcePluginRoot = path.join(linked, 'plugins', 'harness-everything');
|
|
324
|
+
assert.throws(() => compat.install({ codexHome, sourcePluginRoot }), /unreleased|worktree/i);
|
|
325
|
+
assert(!fs.existsSync(codexHome), 'source refusal must happen before writes');
|
|
326
|
+
compat.install({ codexHome, sourcePluginRoot, allowUnreleasedSource: true });
|
|
327
|
+
const packaged = path.join(linked, 'cache', 'harness');
|
|
328
|
+
fs.cpSync(compat.SOURCE_PLUGIN_ROOT, packaged, { recursive: true });
|
|
329
|
+
compat.install({ codexHome: path.join(root, 'linked-cache-home'), sourcePluginRoot: packaged });
|
|
330
|
+
});
|
|
331
|
+
test('CLI forwards explicit unreleased-source opt-in', () => {
|
|
332
|
+
const codexHome = path.join(root, 'cli-home');
|
|
333
|
+
const script = path.resolve(__dirname, '../scripts/codex-hooks-compat.js');
|
|
334
|
+
const result = spawnSync(process.execPath, [script, 'install', '--codex-home', codexHome, '--allow-unreleased-source'], { encoding: 'utf8' });
|
|
335
|
+
assert.strictEqual(result.status, 0, result.stderr);
|
|
336
|
+
});
|
|
337
|
+
} finally {
|
|
338
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
339
|
+
}
|
|
340
|
+
process.exitCode = failures ? 1 : 0;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
8
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-runner-isolation-'));
|
|
9
|
+
try {
|
|
10
|
+
const repo = path.join(root, 'fixture');
|
|
11
|
+
const ci = path.join(repo, 'ci');
|
|
12
|
+
fs.mkdirSync(path.join(ci, 'lib'), { recursive: true });
|
|
13
|
+
fs.copyFileSync(path.join(__dirname, 'mechanism-test.js'), path.join(ci, 'mechanism-test.js'));
|
|
14
|
+
const helper = path.join(__dirname, 'lib', 'isolated-test-process.js');
|
|
15
|
+
if (fs.existsSync(helper)) fs.copyFileSync(helper, path.join(ci, 'lib', 'isolated-test-process.js'));
|
|
16
|
+
const report = path.join(root, 'child.json');
|
|
17
|
+
fs.writeFileSync(path.join(ci, 'mechanism-fixture.test.js'), `
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const assert = require('assert');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
assert.notStrictEqual(process.env.CODEX_HOME, process.env.PARENT_CODEX_HOME);
|
|
23
|
+
assert.notStrictEqual(process.env.HOME, process.env.PARENT_HOME);
|
|
24
|
+
assert.strictEqual(process.env.CODEX_HOME, path.join(process.env.HOME, '.codex'));
|
|
25
|
+
assert.strictEqual(os.homedir(), process.env.HOME);
|
|
26
|
+
fs.mkdirSync(process.env.CODEX_HOME, { recursive: true });
|
|
27
|
+
fs.writeFileSync(path.join(process.env.CODEX_HOME, 'child-proof'), 'isolated');
|
|
28
|
+
fs.writeFileSync(process.env.ISOLATION_REPORT, JSON.stringify({home: process.env.HOME}));
|
|
29
|
+
`);
|
|
30
|
+
const parentHome = path.join(root, 'parent-home');
|
|
31
|
+
const parentCodex = path.join(parentHome, '.codex');
|
|
32
|
+
const result = spawnSync(process.execPath, [path.join(ci, 'mechanism-test.js')], {
|
|
33
|
+
encoding: 'utf8', env: { ...process.env, HOME: parentHome, USERPROFILE: parentHome,
|
|
34
|
+
CODEX_HOME: parentCodex, PARENT_HOME: parentHome, PARENT_CODEX_HOME: parentCodex,
|
|
35
|
+
ISOLATION_REPORT: report },
|
|
36
|
+
});
|
|
37
|
+
assert.strictEqual(result.status, 0, result.stdout + result.stderr);
|
|
38
|
+
const child = JSON.parse(fs.readFileSync(report, 'utf8'));
|
|
39
|
+
assert(!fs.existsSync(child.home), 'runner must clean its temporary home');
|
|
40
|
+
assert(!fs.existsSync(parentHome), 'runner must not write into inherited home');
|
|
41
|
+
// Both entry points must use the same isolated spawn implementation.
|
|
42
|
+
const regression = fs.readFileSync(path.join(__dirname, '../self-evolve/scripts/self-regression.js'), 'utf8');
|
|
43
|
+
assert(regression.includes('spawnTestNode('));
|
|
44
|
+
assert(!regression.includes("spawnSync('node'"));
|
|
45
|
+
console.log('PASS mechanism child receives a temporary CODEX_HOME/HOME; shared self-regression isolation and cleanup verified');
|
|
46
|
+
} finally {
|
|
47
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
48
|
+
}
|