harness-everything 0.26.0 → 0.28.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 +35 -0
- package/behavioral-evals/README.md +39 -0
- package/behavioral-evals/case-validator.js +34 -0
- package/behavioral-evals/cases/baseline-worktree-submodule-reachability.yaml +30 -0
- package/behavioral-evals/fixtures/setup/fail.js +3 -0
- package/behavioral-evals/fixtures/setup/noop.js +4 -0
- package/behavioral-evals/fixtures/setup/verify-worktree-submodule.js +46 -0
- package/behavioral-evals/fixtures/setup/worktree-submodule.js +86 -0
- package/behavioral-evals/paired-benchmark.js +39 -5
- package/behavioral-evals/run-with-plugin.js +53 -39
- package/behavioral-evals/run.js +88 -9
- package/ci/mechanism-2j-behavioral-runner.test.js +121 -1
- package/ci/mechanism-2j-fixture-entrypoints.test.js +153 -0
- package/ci/mechanism-34-worktree-submodule-reachability.test.js +307 -0
- package/ci/mutation-check.js +2 -0
- package/ci/mutation-loader.js +4 -0
- package/ci/test-behavioral-case-validation.js +26 -0
- package/git-commit/SKILL.md +2 -2
- package/git-commit/guides/SUBMODULES.md +54 -1
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/git-commit/SKILL.md +2 -2
- package/plugins/harness-everything/skills/git-commit/guides/SUBMODULES.md +54 -1
- package/plugins/harness-everything/skills/using-git-worktrees/SKILL.md +1 -1
- package/plugins/harness-everything/skills/using-git-worktrees/references/submodules-in-worktrees.md +11 -1
- package/plugins/harness-everything/skills/using-git-worktrees/scripts/submodule-reachability.js +265 -0
- package/sbom.cdx.json +7 -7
- package/using-git-worktrees/SKILL.md +1 -1
- package/using-git-worktrees/references/submodules-in-worktrees.md +11 -1
- package/using-git-worktrees/scripts/submodule-reachability.js +265 -0
package/behavioral-evals/run.js
CHANGED
|
@@ -22,12 +22,13 @@ const fs = require('fs');
|
|
|
22
22
|
const os = require('os');
|
|
23
23
|
const path = require('path');
|
|
24
24
|
const crypto = require('crypto');
|
|
25
|
-
const { execFileSync } = require('child_process');
|
|
25
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
26
26
|
const {
|
|
27
27
|
gradeExecutionEvidence,
|
|
28
28
|
parseTranscriptFile,
|
|
29
29
|
} = require('./transcript-parser');
|
|
30
30
|
const { commandMatches, normalizeRepoPath } = require('../scripts/lib/execution-contract');
|
|
31
|
+
const { resolveFixtureSetup, validateFixture } = require('./case-validator');
|
|
31
32
|
|
|
32
33
|
const ROOT = path.resolve(__dirname, '..');
|
|
33
34
|
const CASES_DIR = path.join(__dirname, 'cases');
|
|
@@ -155,7 +156,8 @@ function validate(cases) {
|
|
|
155
156
|
if (!c.id) problems.push('missing id');
|
|
156
157
|
if (!c.prompt) problems.push('missing prompt');
|
|
157
158
|
if (!c.max_turns) problems.push('missing max_turns');
|
|
158
|
-
if (!c.fixture
|
|
159
|
+
if (!c.fixture) problems.push('fixture missing');
|
|
160
|
+
else problems.push(...validateFixture(c.fixture));
|
|
159
161
|
if (!Array.isArray(c.expectations) || c.expectations.length === 0) problems.push('expectations missing');
|
|
160
162
|
if (c.loaded_skills !== undefined && (!Array.isArray(c.loaded_skills) || c.loaded_skills.some(skill => !fs.existsSync(path.join(ROOT, skill, 'SKILL.md'))))) {
|
|
161
163
|
problems.push('loaded_skills must name existing skills');
|
|
@@ -207,6 +209,66 @@ function buildWorkspace(c) {
|
|
|
207
209
|
return ws;
|
|
208
210
|
}
|
|
209
211
|
|
|
212
|
+
function runFixtureSetup(c, ws) {
|
|
213
|
+
const resolved = resolveFixtureSetup(c && c.fixture && c.fixture.setup);
|
|
214
|
+
if (!resolved) return { path: null, script_sha256: null, status: 'not-requested', exit_code: null, duration_ms: 0 };
|
|
215
|
+
const scriptSha256 = sha256(fs.readFileSync(resolved.absolute));
|
|
216
|
+
const startedAt = Date.now();
|
|
217
|
+
const result = spawnSync(process.execPath, [resolved.absolute], {
|
|
218
|
+
cwd: ws,
|
|
219
|
+
encoding: 'utf8',
|
|
220
|
+
env: {
|
|
221
|
+
...process.env,
|
|
222
|
+
HARNESS_BEHAVIORAL_WORKSPACE: ws,
|
|
223
|
+
HARNESS_BEHAVIORAL_CASE_ID: String(c.id || ''),
|
|
224
|
+
},
|
|
225
|
+
timeout: 60 * 1000,
|
|
226
|
+
windowsHide: true,
|
|
227
|
+
});
|
|
228
|
+
const evidence = {
|
|
229
|
+
path: resolved.relative,
|
|
230
|
+
script_sha256: scriptSha256,
|
|
231
|
+
status: result.status === 0 && !result.error ? 'pass' : 'fail',
|
|
232
|
+
exit_code: result.status,
|
|
233
|
+
duration_ms: Date.now() - startedAt,
|
|
234
|
+
};
|
|
235
|
+
if (result.error || result.status !== 0) {
|
|
236
|
+
const detail = (result.stderr || result.stdout || (result.error && result.error.message) || '').trim();
|
|
237
|
+
const error = new Error(
|
|
238
|
+
`fixture setup failed: ${resolved.relative}${result.status === null ? '' : ` (exit ${result.status})`}${detail ? `: ${detail.slice(0, 300)}` : ''}`
|
|
239
|
+
);
|
|
240
|
+
error.fixtureSetup = evidence;
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
return evidence;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function prepareWorkspace(c) {
|
|
247
|
+
let workspace = null;
|
|
248
|
+
try {
|
|
249
|
+
workspace = buildWorkspace(c);
|
|
250
|
+
return {
|
|
251
|
+
ok: true,
|
|
252
|
+
workspace,
|
|
253
|
+
setup: runFixtureSetup(c, workspace),
|
|
254
|
+
};
|
|
255
|
+
} catch (error) {
|
|
256
|
+
return {
|
|
257
|
+
ok: false,
|
|
258
|
+
workspace,
|
|
259
|
+
outcome: 'fixture-error',
|
|
260
|
+
error: error.message.slice(0, 500),
|
|
261
|
+
setup: error.fixtureSetup || {
|
|
262
|
+
path: c && c.fixture ? c.fixture.setup || null : null,
|
|
263
|
+
script_sha256: null,
|
|
264
|
+
status: 'fail',
|
|
265
|
+
exit_code: null,
|
|
266
|
+
duration_ms: null,
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
210
272
|
// Capture the requested fixture baseline after treatment installation so the
|
|
211
273
|
// installer's own artifacts (.claude/, .harness-src/, opencode.json) show up
|
|
212
274
|
// in neither arm's post-run scope check.
|
|
@@ -685,9 +747,8 @@ function treatmentSkills(c) {
|
|
|
685
747
|
function runArm(c, engine, arm, pairId) {
|
|
686
748
|
const loadedSkills = arm === 'treatment' ? treatmentSkills(c) : [];
|
|
687
749
|
const attributionRequired = arm === 'treatment';
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
if (c.fixture.git) gitSnapshot(ws);
|
|
750
|
+
const prepared = prepareWorkspace(c);
|
|
751
|
+
const ws = prepared.workspace;
|
|
691
752
|
|
|
692
753
|
const record = {
|
|
693
754
|
arm,
|
|
@@ -698,9 +759,22 @@ function runArm(c, engine, arm, pairId) {
|
|
|
698
759
|
workspace: ws,
|
|
699
760
|
fixture_sha256: sha256(JSON.stringify(c.fixture)),
|
|
700
761
|
prompt_sha256: sha256(c.prompt),
|
|
762
|
+
fixture_setup: prepared.setup,
|
|
701
763
|
cost: null,
|
|
702
764
|
};
|
|
765
|
+
if (!prepared.ok) {
|
|
766
|
+
return {
|
|
767
|
+
...record,
|
|
768
|
+
outcome: 'fixture-error',
|
|
769
|
+
error: prepared.error,
|
|
770
|
+
parse_status: 'unavailable',
|
|
771
|
+
tool_call_count: null,
|
|
772
|
+
tool_call_counts: { attempted: null, completed: null, denied: null, unresolved: null },
|
|
773
|
+
};
|
|
774
|
+
}
|
|
703
775
|
try {
|
|
776
|
+
if (arm === 'treatment') installHarness(ws, loadedSkills);
|
|
777
|
+
if (c.fixture.git) gitSnapshot(ws);
|
|
704
778
|
const transcriptPath = runHeadless(c.prompt, ws, c.max_turns, engine, arm);
|
|
705
779
|
const graded = grade(c, ws, transcriptPath, engine);
|
|
706
780
|
const { results, passed } = graded;
|
|
@@ -764,6 +838,8 @@ function summarizePairResults(pairResults) {
|
|
|
764
838
|
const completed = pairResults.filter(isCompletedPair);
|
|
765
839
|
const sessionFailures = pairResults.filter(record => Object.values(record.arms || {})
|
|
766
840
|
.some(arm => arm.outcome === 'session-error')).length;
|
|
841
|
+
const fixtureFailures = pairResults.filter(record => Object.values(record.arms || {})
|
|
842
|
+
.some(arm => arm.outcome === 'fixture-error')).length;
|
|
767
843
|
const effective = completed.filter(record => record.verdict === 'EFFECTIVE').length;
|
|
768
844
|
const categorySummary = {};
|
|
769
845
|
for (const record of pairResults) {
|
|
@@ -806,6 +882,7 @@ function summarizePairResults(pairResults) {
|
|
|
806
882
|
cost,
|
|
807
883
|
completed_pairs: completed.length,
|
|
808
884
|
session_failures: sessionFailures,
|
|
885
|
+
fixture_failures: fixtureFailures,
|
|
809
886
|
tool_call_delta: toolCallDelta,
|
|
810
887
|
verdicts: Object.fromEntries(['EFFECTIVE', 'INEFFECTIVE', 'INCONCLUSIVE', 'HARMFUL']
|
|
811
888
|
.map(verdict => [verdict, completed.filter(record => record.verdict === verdict).length])),
|
|
@@ -835,7 +912,7 @@ function runLive(filter, engineArg, armArg = 'treatment') {
|
|
|
835
912
|
fs.mkdirSync(RESULTS_DIR, { recursive: true });
|
|
836
913
|
|
|
837
914
|
const pairResults = [];
|
|
838
|
-
let
|
|
915
|
+
let infrastructureFailures = 0;
|
|
839
916
|
for (const c of cases) {
|
|
840
917
|
const pairId = `${c.id}-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
|
|
841
918
|
const order = armArg === 'both'
|
|
@@ -864,13 +941,13 @@ function runLive(filter, engineArg, armArg = 'treatment') {
|
|
|
864
941
|
for (const arm of Object.values(arms)) {
|
|
865
942
|
console.log(` ${arm.arm}: ${arm.outcome} (${arm.loaded_skills.length ? arm.loaded_skills.join(', ') : 'no Harness skills'})`);
|
|
866
943
|
}
|
|
867
|
-
if (Object.values(arms).some(arm => arm.outcome === 'session-error'))
|
|
944
|
+
if (Object.values(arms).some(arm => arm.outcome === 'session-error' || arm.outcome === 'fixture-error')) infrastructureFailures++;
|
|
868
945
|
if (armArg === 'both') console.log(` Verdict: ${record.verdict}`);
|
|
869
946
|
const suffix = armArg === 'both' ? 'pair' : armArg;
|
|
870
947
|
fs.writeFileSync(path.join(RESULTS_DIR, `${record.date.slice(0, 10)}-${c.id}-${suffix}.json`), JSON.stringify(record, null, 2));
|
|
871
948
|
}
|
|
872
949
|
|
|
873
|
-
let exitCode =
|
|
950
|
+
let exitCode = infrastructureFailures ? 1 : 0;
|
|
874
951
|
if (armArg === 'both') {
|
|
875
952
|
const aggregate = summarizePairResults(pairResults);
|
|
876
953
|
const summary = {
|
|
@@ -888,7 +965,7 @@ function runLive(filter, engineArg, armArg = 'treatment') {
|
|
|
888
965
|
fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2));
|
|
889
966
|
console.log(`\nPaired summary: ${summaryPath}`);
|
|
890
967
|
console.log(`Completed pairs: ${summary.completed_pairs}/${summary.requested_sample_size}`);
|
|
891
|
-
exitCode = aggregate.session_failures ? 1 : 0;
|
|
968
|
+
exitCode = (aggregate.session_failures || aggregate.fixture_failures) ? 1 : 0;
|
|
892
969
|
}
|
|
893
970
|
process.exit(exitCode);
|
|
894
971
|
}
|
|
@@ -915,6 +992,8 @@ if (require.main === module) main();
|
|
|
915
992
|
module.exports = {
|
|
916
993
|
buildEngineInvocation,
|
|
917
994
|
buildWorkspace,
|
|
995
|
+
runFixtureSetup,
|
|
996
|
+
prepareWorkspace,
|
|
918
997
|
countToolCalls,
|
|
919
998
|
extractAgentEvents,
|
|
920
999
|
extractAgentTrace,
|
|
@@ -2,7 +2,8 @@ const helper = require('./test-helper');
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const {
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
const { buildEngineInvocation, buildWorkspace, runFixtureSetup, prepareWorkspace, summarizePairResults } = require('../behavioral-evals/run');
|
|
6
7
|
|
|
7
8
|
console.log('\n[2j] Behavioral runner argv integrity...');
|
|
8
9
|
const prompt = "Add punctuation stripping to slug.js, then run npm test. Do not truncate this request.";
|
|
@@ -59,4 +60,123 @@ helper.check(
|
|
|
59
60
|
traversalError && traversalError.stack ? traversalError.stack : traversalTarget
|
|
60
61
|
);
|
|
61
62
|
|
|
63
|
+
let setupWorkspace = null;
|
|
64
|
+
try {
|
|
65
|
+
setupWorkspace = buildWorkspace({
|
|
66
|
+
id: 'fixture-setup-valid',
|
|
67
|
+
fixture: {
|
|
68
|
+
files: [{ path: 'seed.txt', content: 'seed' }],
|
|
69
|
+
setup: 'behavioral-evals/fixtures/setup/noop.js',
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
const setupResult = runFixtureSetup({
|
|
73
|
+
id: 'fixture-setup-valid',
|
|
74
|
+
fixture: { setup: 'behavioral-evals/fixtures/setup/noop.js' },
|
|
75
|
+
}, setupWorkspace);
|
|
76
|
+
helper.check(
|
|
77
|
+
'2j. fixture setup runs via Node in the generated workspace',
|
|
78
|
+
setupResult.status === 'pass' &&
|
|
79
|
+
fs.readFileSync(path.join(setupWorkspace, '.fixture-setup-ok'), 'utf8') === 'ok\n',
|
|
80
|
+
JSON.stringify(setupResult)
|
|
81
|
+
);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
helper.check('2j. fixture setup runs via Node in the generated workspace', false, error && error.stack ? error.stack : String(error));
|
|
84
|
+
} finally {
|
|
85
|
+
if (setupWorkspace) fs.rmSync(setupWorkspace, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let failedPreparation = null;
|
|
89
|
+
try {
|
|
90
|
+
failedPreparation = prepareWorkspace({
|
|
91
|
+
id: 'fixture-setup-fails',
|
|
92
|
+
fixture: {
|
|
93
|
+
files: [{ path: 'seed.txt', content: 'seed' }],
|
|
94
|
+
setup: 'behavioral-evals/fixtures/setup/fail.js',
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
} catch (error) {
|
|
98
|
+
failedPreparation = { threw: error && error.stack ? error.stack : String(error) };
|
|
99
|
+
}
|
|
100
|
+
helper.check(
|
|
101
|
+
'2j. setup failure is classified as fixture-error before any model session',
|
|
102
|
+
Boolean(
|
|
103
|
+
failedPreparation &&
|
|
104
|
+
failedPreparation.ok === false &&
|
|
105
|
+
failedPreparation.outcome === 'fixture-error' &&
|
|
106
|
+
failedPreparation.setup &&
|
|
107
|
+
failedPreparation.setup.status === 'fail' &&
|
|
108
|
+
failedPreparation.setup.exit_code === 17
|
|
109
|
+
),
|
|
110
|
+
JSON.stringify(failedPreparation)
|
|
111
|
+
);
|
|
112
|
+
if (failedPreparation && failedPreparation.workspace) {
|
|
113
|
+
fs.rmSync(failedPreparation.workspace, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let topologyPreparation = null;
|
|
117
|
+
try {
|
|
118
|
+
topologyPreparation = prepareWorkspace({
|
|
119
|
+
id: 'worktree-submodule-setup-smoke',
|
|
120
|
+
fixture: {
|
|
121
|
+
files: [{ path: 'README.md', content: 'topology smoke' }],
|
|
122
|
+
setup: 'behavioral-evals/fixtures/setup/worktree-submodule.js',
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
const linkedSub = topologyPreparation.ok
|
|
126
|
+
? path.join(topologyPreparation.workspace, 'super-wt', 'libs', 'sub')
|
|
127
|
+
: null;
|
|
128
|
+
const branchResult = linkedSub
|
|
129
|
+
? spawnSync('git', ['-C', linkedSub, 'branch', '--show-current'], { encoding: 'utf8', windowsHide: true })
|
|
130
|
+
: null;
|
|
131
|
+
const helperResult = topologyPreparation.ok
|
|
132
|
+
? spawnSync(process.execPath, [
|
|
133
|
+
path.join(topologyPreparation.workspace, '.fixture-tools', 'submodule-reachability.js'),
|
|
134
|
+
'--json', '--root', 'super-wt',
|
|
135
|
+
], { cwd: topologyPreparation.workspace, encoding: 'utf8', windowsHide: true })
|
|
136
|
+
: null;
|
|
137
|
+
helper.check(
|
|
138
|
+
'2j. worktree/submodule setup creates the real #243 topology before model execution',
|
|
139
|
+
Boolean(
|
|
140
|
+
topologyPreparation.ok &&
|
|
141
|
+
fs.existsSync(path.join(topologyPreparation.workspace, 'super-wt', '.git')) &&
|
|
142
|
+
fs.existsSync(path.join(linkedSub, '.git')) &&
|
|
143
|
+
branchResult && branchResult.status === 0 && branchResult.stdout.trim() === '' &&
|
|
144
|
+
helperResult && helperResult.status === 0
|
|
145
|
+
),
|
|
146
|
+
JSON.stringify({
|
|
147
|
+
preparation: topologyPreparation,
|
|
148
|
+
branchStatus: branchResult && branchResult.status,
|
|
149
|
+
branch: branchResult && branchResult.stdout,
|
|
150
|
+
helperStatus: helperResult && helperResult.status,
|
|
151
|
+
helperStderr: helperResult && helperResult.stderr,
|
|
152
|
+
})
|
|
153
|
+
);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
helper.check(
|
|
156
|
+
'2j. worktree/submodule setup creates the real #243 topology before model execution',
|
|
157
|
+
false,
|
|
158
|
+
error && error.stack ? error.stack : String(error)
|
|
159
|
+
);
|
|
160
|
+
} finally {
|
|
161
|
+
if (topologyPreparation && topologyPreparation.workspace) {
|
|
162
|
+
fs.rmSync(topologyPreparation.workspace, { recursive: true, force: true });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const fixtureFailureSummary = summarizePairResults([{
|
|
167
|
+
id: 'fixture-error-pair',
|
|
168
|
+
pressure_category: null,
|
|
169
|
+
arms: {
|
|
170
|
+
baseline: { outcome: 'fixture-error', cost: null, tool_call_count: null },
|
|
171
|
+
treatment: { outcome: 'pass', cost: null, tool_call_count: 1 },
|
|
172
|
+
},
|
|
173
|
+
}]);
|
|
174
|
+
helper.check(
|
|
175
|
+
'2j. fixture errors remain distinct aggregate infrastructure failures',
|
|
176
|
+
fixtureFailureSummary.fixture_failures === 1 &&
|
|
177
|
+
fixtureFailureSummary.session_failures === 0 &&
|
|
178
|
+
fixtureFailureSummary.completed_pairs === 0,
|
|
179
|
+
JSON.stringify(fixtureFailureSummary)
|
|
180
|
+
);
|
|
181
|
+
|
|
62
182
|
helper.finish();
|
|
@@ -0,0 +1,153 @@
|
|
|
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 { parseSimpleYaml, prepareWorkspace } = require('../behavioral-evals/run');
|
|
9
|
+
const paired = require('../behavioral-evals/paired-benchmark');
|
|
10
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
11
|
+
const topology = parseSimpleYaml(fs.readFileSync(path.join(ROOT, 'behavioral-evals/cases/baseline-worktree-submodule-reachability.yaml'), 'utf8'));
|
|
12
|
+
const failure = { ...topology, fixture: { ...topology.fixture, setup: 'behavioral-evals/fixtures/setup/fail.js' } };
|
|
13
|
+
const paths = [];
|
|
14
|
+
const topologyFingerprints = [];
|
|
15
|
+
const setupHashes = [];
|
|
16
|
+
function track(ws) { if (ws) paths.push(ws); return ws; }
|
|
17
|
+
function gitText(cwd, args) {
|
|
18
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8', windowsHide: true });
|
|
19
|
+
assert.strictEqual(result.status, 0, result.stderr || result.stdout);
|
|
20
|
+
return result.stdout.trim();
|
|
21
|
+
}
|
|
22
|
+
function topologyFingerprint(ws) {
|
|
23
|
+
return {
|
|
24
|
+
child_head: gitText(path.join(ws, 'child-origin'), ['rev-parse', 'HEAD']),
|
|
25
|
+
super_head: gitText(path.join(ws, 'super'), ['rev-parse', 'HEAD']),
|
|
26
|
+
linked_head: gitText(path.join(ws, 'super-wt'), ['rev-parse', 'HEAD']),
|
|
27
|
+
gitlink: gitText(path.join(ws, 'super-wt'), ['rev-parse', 'HEAD:libs/sub']),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function assertTopology(ws) {
|
|
31
|
+
assert.ok(fs.existsSync(path.join(ws, 'super-wt', '.git')), 'setup must construct the linked worktree');
|
|
32
|
+
assert.ok(fs.existsSync(path.join(ws, '.fixture-tools', 'verify-worktree-submodule.js')), 'setup must install the grader');
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
for (const arm of ['baseline', 'treatment']) {
|
|
36
|
+
for (const effect of ['skill-text', 'plugin-enforcement']) {
|
|
37
|
+
const prepared = paired.prepareArmWorkspace(topology, effect, arm, 'opencode');
|
|
38
|
+
track(prepared.ws);
|
|
39
|
+
assertTopology(prepared.ws);
|
|
40
|
+
assert.strictEqual(prepared.fixture_setup.status, 'pass');
|
|
41
|
+
assert.match(prepared.fixture_setup.script_sha256, /^[0-9a-f]{64}$/);
|
|
42
|
+
topologyFingerprints.push(topologyFingerprint(prepared.ws));
|
|
43
|
+
setupHashes.push(prepared.fixture_setup.script_sha256);
|
|
44
|
+
const failed = paired.prepareArmWorkspace(failure, effect, arm, 'opencode');
|
|
45
|
+
track(failed.ws);
|
|
46
|
+
assert.strictEqual(failed.ok, false);
|
|
47
|
+
assert.strictEqual(failed.outcome, 'fixture-error');
|
|
48
|
+
assert.strictEqual(failed.fixture_setup.exit_code, 17);
|
|
49
|
+
assert.ok(!fs.existsSync(path.join(failed.ws, '.claude')), 'setup failure must precede skill installation');
|
|
50
|
+
assert.ok(!fs.existsSync(path.join(failed.ws, '.opencode')), 'setup failure must precede plugin installation');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
assert.ok(topologyFingerprints.length >= 4);
|
|
54
|
+
for (const fingerprint of topologyFingerprints.slice(1)) {
|
|
55
|
+
assert.deepStrictEqual(fingerprint, topologyFingerprints[0], 'fixture setup must produce identical Git object identities across independent arms/runs');
|
|
56
|
+
}
|
|
57
|
+
assert.strictEqual(new Set(setupHashes).size, 1, 'all arms must retain the same checked-in setup script hash');
|
|
58
|
+
|
|
59
|
+
const pairDir = track(fs.mkdtempSync(path.join(os.tmpdir(), 'fixture-entrypoints-')));
|
|
60
|
+
let launches = 0;
|
|
61
|
+
const context = { effect_type: 'skill-text', engine: 'claude', model: 'test' };
|
|
62
|
+
const arms = {};
|
|
63
|
+
for (const arm of ['baseline', 'treatment']) {
|
|
64
|
+
arms[arm] = paired.runArm(failure, context, arm, pairDir, () => { launches++; throw new Error('model must not start'); });
|
|
65
|
+
assert.strictEqual(arms[arm].outcome, 'fixture-error');
|
|
66
|
+
assert.strictEqual(arms[arm].fixture_setup.exit_code, 17);
|
|
67
|
+
assert.strictEqual(arms[arm].tool_call_count, null);
|
|
68
|
+
}
|
|
69
|
+
assert.strictEqual(launches, 0);
|
|
70
|
+
const summary = paired.summarizePairs([{ id: topology.id, arms }], 10);
|
|
71
|
+
assert.strictEqual(summary.fixture_failures, 1);
|
|
72
|
+
assert.strictEqual(summary.included_pairs, 0);
|
|
73
|
+
assert.deepStrictEqual(summary.arm_outcomes, { baseline: { pass: 0, fail: 0 }, treatment: { pass: 0, fail: 0 } });
|
|
74
|
+
|
|
75
|
+
const plugin = require('../behavioral-evals/run-with-plugin');
|
|
76
|
+
const failed = plugin.runCase(failure, () => { launches++; throw new Error('model must not start'); });
|
|
77
|
+
track(failed.workspace);
|
|
78
|
+
assert.strictEqual(failed.outcome, 'fixture-error');
|
|
79
|
+
assert.strictEqual(failed.fixture_setup.exit_code, 17);
|
|
80
|
+
assert.strictEqual(launches, 0);
|
|
81
|
+
assert.ok(!fs.existsSync(path.join(failed.workspace, '.harness-src')), 'failed setup must precede Harness installation');
|
|
82
|
+
|
|
83
|
+
// Exercise the same public case path as the plugin CLI, without calling a model.
|
|
84
|
+
const result = plugin.runCase(topology, (_prompt, ws) => {
|
|
85
|
+
launches++;
|
|
86
|
+
assertTopology(ws);
|
|
87
|
+
const transcript = path.join(pairDir, 'plugin-transcript.jsonl');
|
|
88
|
+
fs.writeFileSync(transcript, '');
|
|
89
|
+
return transcript;
|
|
90
|
+
});
|
|
91
|
+
track(result.workspace);
|
|
92
|
+
assert.strictEqual(launches, 1);
|
|
93
|
+
assert.strictEqual(result.fixture_setup.status, 'pass');
|
|
94
|
+
assert.strictEqual(result.outcome, 'fail', 'an untouched topology must fail the actual final-state graders');
|
|
95
|
+
|
|
96
|
+
for (const fixture of [
|
|
97
|
+
{ files: [{ path: '../escape', content: 'bad' }] },
|
|
98
|
+
{ files: [{ path: 'seed', content: 'ok' }], setup: '../escape.js' },
|
|
99
|
+
]) {
|
|
100
|
+
const result = prepareWorkspace({ id: 'invalid-fixture', fixture });
|
|
101
|
+
track(result.workspace);
|
|
102
|
+
assert.strictEqual(result.outcome, 'fixture-error');
|
|
103
|
+
}
|
|
104
|
+
// The public CLIs must retain fixture-error evidence and exit nonzero.
|
|
105
|
+
const caseId = `fixture-entrypoints-failure-${process.pid}`;
|
|
106
|
+
const casePath = path.join(ROOT, 'behavioral-evals/cases', `${caseId}.yaml`);
|
|
107
|
+
fs.writeFileSync(casePath, `id: ${caseId}
|
|
108
|
+
name: Fixture failure CLI control
|
|
109
|
+
loaded_skills:
|
|
110
|
+
- git-commit
|
|
111
|
+
fixture:
|
|
112
|
+
files:
|
|
113
|
+
- path: seed.txt
|
|
114
|
+
content: seed
|
|
115
|
+
setup: behavioral-evals/fixtures/setup/fail.js
|
|
116
|
+
prompt: This model session must never start.
|
|
117
|
+
max_turns: 1
|
|
118
|
+
expectations:
|
|
119
|
+
- type: file_contains
|
|
120
|
+
path: seed.txt
|
|
121
|
+
value: seed
|
|
122
|
+
`);
|
|
123
|
+
paths.push(casePath);
|
|
124
|
+
const pairedCli = spawnSync(process.execPath, [
|
|
125
|
+
path.join(ROOT, 'behavioral-evals/paired-benchmark.js'), 'run',
|
|
126
|
+
'--effect', 'skill-text', '--engine', 'claude', '--model', 'fixture-test',
|
|
127
|
+
'--min-effect-pp', '10', '--case', caseId,
|
|
128
|
+
], { cwd: ROOT, encoding: 'utf8', timeout: 30000 });
|
|
129
|
+
const summaryMatch = pairedCli.stdout.match(/Summary: (.+)/);
|
|
130
|
+
assert.ok(summaryMatch, pairedCli.stderr || pairedCli.stdout);
|
|
131
|
+
const summaryPath = summaryMatch[1].trim();
|
|
132
|
+
track(path.dirname(summaryPath));
|
|
133
|
+
assert.strictEqual(pairedCli.status, 1, pairedCli.stderr);
|
|
134
|
+
const cliSummary = JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
|
|
135
|
+
assert.strictEqual(cliSummary.fixture_failures, 1);
|
|
136
|
+
assert.strictEqual(cliSummary.included_pairs, 0);
|
|
137
|
+
const pluginCli = spawnSync(process.execPath, [
|
|
138
|
+
path.join(ROOT, 'behavioral-evals/run-with-plugin.js'), 'run', '--case', caseId,
|
|
139
|
+
], { cwd: ROOT, encoding: 'utf8', timeout: 30000 });
|
|
140
|
+
assert.strictEqual(pluginCli.status, 1, pluginCli.stderr);
|
|
141
|
+
const resultMatch = pluginCli.stdout.match(/fixture-error -> (.+)/);
|
|
142
|
+
assert.ok(resultMatch, pluginCli.stdout);
|
|
143
|
+
const resultPath = resultMatch[1].trim();
|
|
144
|
+
paths.push(resultPath);
|
|
145
|
+
const pluginFailure = JSON.parse(fs.readFileSync(resultPath, 'utf8'));
|
|
146
|
+
track(pluginFailure.workspace);
|
|
147
|
+
assert.strictEqual(pluginFailure.fixture_setup.exit_code, 17);
|
|
148
|
+
assert.ok(!fs.existsSync(path.join(pluginFailure.workspace, '.harness-src')));
|
|
149
|
+
|
|
150
|
+
console.log('Fixture entrypoints: paired baseline/treatment and plugin execute setup; failures skip installation/model and are excluded from scores.');
|
|
151
|
+
} finally {
|
|
152
|
+
for (const dir of paths) fs.rmSync(dir, { recursive: true, force: true });
|
|
153
|
+
}
|