@punal_manalan/blender-project-harness 0.0.0-stage → 1.0.2
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/.kilo/agent/blender-grader.md +197 -0
- package/.kilo/agent/blender-worker.md +127 -0
- package/.kilo/blender-project-harness/config.example.json +71 -0
- package/.kilo/blender-project-harness/tools/harness/EXPECTED_FLOW.md +656 -0
- package/.kilo/blender-project-harness/tools/harness/blender-operations.mjs +882 -0
- package/.kilo/blender-project-harness/tools/harness/blender-operations.test.mjs +550 -0
- package/.kilo/blender-project-harness/tools/harness/blender.mjs +301 -0
- package/.kilo/blender-project-harness/tools/harness/blender.test.mjs +87 -0
- package/.kilo/blender-project-harness/tools/harness/cli.mjs +127 -0
- package/.kilo/blender-project-harness/tools/harness/documents.mjs +201 -0
- package/.kilo/blender-project-harness/tools/harness/download.mjs +94 -0
- package/.kilo/blender-project-harness/tools/harness/download.test.mjs +63 -0
- package/.kilo/blender-project-harness/tools/harness/engine.mjs +1866 -0
- package/.kilo/blender-project-harness/tools/harness/engine.test.mjs +2219 -0
- package/.kilo/blender-project-harness/tools/harness/intake.mjs +571 -0
- package/.kilo/blender-project-harness/tools/harness/intake.test.mjs +181 -0
- package/.kilo/blender-project-harness/tools/harness/package-lock.json +1227 -0
- package/.kilo/blender-project-harness/tools/harness/package.json +15 -0
- package/.kilo/blender-project-harness/tools/harness/plugin.mjs +550 -0
- package/.kilo/blender-project-harness/tools/harness/plugin.test.mjs +526 -0
- package/.kilo/blender-project-harness/tools/harness/proxy.mjs +222 -0
- package/.kilo/blender-project-harness/tools/harness/proxy.test.mjs +217 -0
- package/.kilo/blender-project-harness/tools/harness/scaffold.mjs +83 -0
- package/.kilo/blender-project-harness/tools/harness/scaffold.test.mjs +74 -0
- package/.kilo/blender-project-harness/tools/harness/server.mjs +24 -0
- package/.kilo/blender-project-harness/tools/harness/shell-policy.mjs +196 -0
- package/.kilo/blender-project-harness/tools/harness/shell-policy.test.mjs +68 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/ASSETS.md +29 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/DECISIONS.md +8 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/Goals/01-project-brief.md +71 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/Goals/README.md +17 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/INTAKE.md +8 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/README.md +57 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/REQUIREMENTS.md +64 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/REVIEW.md +29 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/References/README.md +18 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/Reviews/README.md +48 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/TASKSTRACKER.md +10 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/Tasks/README.md +4 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/WORKFLOW.md +36 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/README.md +130 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/assets/README.md +31 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/assets/audio/.gitkeep +0 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/assets/external/README.md +43 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/assets/external/SOURCE_TEMPLATE.md +18 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/assets/hdri/.gitkeep +0 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/assets/models/.gitkeep +0 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/assets/textures/.gitkeep +0 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/backups/.gitkeep +0 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/backups/README.md +5 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/exports/.gitkeep +0 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/exports/README.md +11 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/renders/README.md +7 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/renders/final/.gitkeep +0 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/renders/wip/.gitkeep +0 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/scenes/README.md +10 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/scripts/README.md +16 -0
- package/.kilo/blender-project-harness/tools/harness/test-fixture.mjs +91 -0
- package/.kilo/blender-project-harness/tools/harness/workspace.mjs +30 -0
- package/.kilo/blender-project-harness/tools/harness/workspace.test.mjs +31 -0
- package/.kilo/blender-project-harness/tools/test_blender_tools.py +427 -0
- package/.kilo/plugins/blender-workflow.js +3 -0
- package/.kilo/skills/blender-assets/SKILL.md +99 -0
- package/.kilo/skills/blender-mcp/SKILL.md +147 -0
- package/.kilo/skills/blender-mcp/references/README.md +22 -0
- package/.kilo/skills/blender-mcp/references/animation.md +83 -0
- package/.kilo/skills/blender-mcp/references/bpy.md +97 -0
- package/.kilo/skills/blender-mcp/references/level-design.md +53 -0
- package/.kilo/skills/blender-mcp/references/materials.md +80 -0
- package/.kilo/skills/blender-mcp/references/retopology.md +104 -0
- package/.kilo/skills/blender-mcp/references/rigging.md +123 -0
- package/.kilo/skills/blender-mcp/references/scene.md +72 -0
- package/.kilo/skills/blender-project-goal/SKILL.md +115 -0
- package/.kilo/skills/blender-project-grading/SKILL.md +184 -0
- package/.kilo/skills/blender-project-readme/SKILL.md +46 -0
- package/.kilo/skills/blender-project-requirements/SKILL.md +84 -0
- package/.kilo/skills/blender-project-scaffold/SKILL.md +59 -0
- package/.kilo/skills/blender-project-workflow/SKILL.md +96 -0
- package/.kilo/skills/blender-subagents/SKILL.md +204 -0
- package/.kilo/skills/blender-workflow-harness/SKILL.md +187 -0
- package/AGENTS.md +234 -0
- package/Blender/README.md +30 -0
- package/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +368 -2
- package/THIRD_PARTY_NOTICES.md +51 -0
- package/bin/blender-project-harness.mjs +4 -0
- package/installer/cli.mjs +116 -0
- package/installer/install.mjs +325 -0
- package/installer/install.test.mjs +134 -0
- package/package.json +58 -6
|
@@ -0,0 +1,2219 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
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 test from 'node:test';
|
|
8
|
+
import { Harness, contained, externalBinaryPolicy } from './engine.mjs';
|
|
9
|
+
|
|
10
|
+
const tempRoot = path.join(os.tmpdir(), 'kilo');
|
|
11
|
+
fs.mkdirSync(tempRoot, { recursive: true });
|
|
12
|
+
const PNG_1X1 = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl2ZQAAAABJRU5ErkJggg==', 'base64');
|
|
13
|
+
const GLB = Buffer.concat([Buffer.from('glTF'), Buffer.from([2, 0, 0, 0]), Buffer.alloc(8)]);
|
|
14
|
+
|
|
15
|
+
function git(root, args, { allowFailure = false } = {}) {
|
|
16
|
+
const result = spawnSync('git', args, {
|
|
17
|
+
cwd: root,
|
|
18
|
+
encoding: 'utf8',
|
|
19
|
+
windowsHide: true,
|
|
20
|
+
timeout: 30_000,
|
|
21
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
22
|
+
});
|
|
23
|
+
if (!allowFailure && result.status !== 0) {
|
|
24
|
+
throw new Error(`git ${args.join(' ')} failed: ${result.stderr?.trim() || result.error?.message}`);
|
|
25
|
+
}
|
|
26
|
+
return result.stdout ?? '';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function write(root, relative, content) {
|
|
30
|
+
const full = path.join(root, ...relative.split('/'));
|
|
31
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
32
|
+
fs.writeFileSync(full, relative.endsWith('.md') && typeof content === 'string'
|
|
33
|
+
? content.replace(/\r?\n/g, '\r\n') : content);
|
|
34
|
+
return full;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function read(root, relative) {
|
|
38
|
+
return fs.readFileSync(path.join(root, ...relative.split('/')), 'utf8');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function commit(root, message, files = ['.']) {
|
|
42
|
+
git(root, ['add', '--', ...files]);
|
|
43
|
+
git(root, ['-c', 'user.name=Harness Tests', '-c', 'user.email=harness-tests@example.invalid', 'commit', '-m', message]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validGraderConfig(overrides = {}) {
|
|
47
|
+
return {
|
|
48
|
+
blender_subagents: {
|
|
49
|
+
graders: {
|
|
50
|
+
agent: 'blender-grader',
|
|
51
|
+
enabled: true,
|
|
52
|
+
max_parallel: 1,
|
|
53
|
+
max_rounds: 1,
|
|
54
|
+
graders: [{
|
|
55
|
+
name: 'visual',
|
|
56
|
+
focus: 'composition and materials',
|
|
57
|
+
provider: 'fixture-provider',
|
|
58
|
+
model: 'fixture-model',
|
|
59
|
+
variant: 'fixture-variant',
|
|
60
|
+
backup: { provider: 'backup-provider', model: 'backup-model', variant: 'backup-variant' },
|
|
61
|
+
max_minutes: 1,
|
|
62
|
+
max_steps: 4,
|
|
63
|
+
}],
|
|
64
|
+
...overrides,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function threeGraderConfig() {
|
|
71
|
+
const grader = (name, focus) => ({
|
|
72
|
+
name,
|
|
73
|
+
focus,
|
|
74
|
+
provider: 'fixture-provider',
|
|
75
|
+
model: 'fixture-model',
|
|
76
|
+
variant: 'fixture-variant',
|
|
77
|
+
backup: { provider: 'backup-provider', model: 'backup-model', variant: 'backup-variant' },
|
|
78
|
+
max_minutes: 5,
|
|
79
|
+
max_steps: 10,
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
blender_subagents: {
|
|
83
|
+
graders: {
|
|
84
|
+
agent: 'blender-grader',
|
|
85
|
+
enabled: true,
|
|
86
|
+
max_parallel: 3,
|
|
87
|
+
max_rounds: 2,
|
|
88
|
+
graders: [
|
|
89
|
+
grader('visual', 'composition, camera, lighting, and materials'),
|
|
90
|
+
grader('technical', 'geometry, topology, render settings, and scene health'),
|
|
91
|
+
grader('spec', 'requirements and acceptance fidelity'),
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function createProject(root, {
|
|
99
|
+
name = 'Test',
|
|
100
|
+
workflow = 'Fully Autonomous',
|
|
101
|
+
review = 'Off',
|
|
102
|
+
goalText,
|
|
103
|
+
} = {}) {
|
|
104
|
+
const project = `Blender/${name}`;
|
|
105
|
+
const goal = `${project}/Documents/Goals/01-test-goal.md`;
|
|
106
|
+
const sceneName = 'scene.blend';
|
|
107
|
+
const scene = `${project}/${sceneName}`;
|
|
108
|
+
const prompt = goalText ?? [
|
|
109
|
+
'Mission: exercise a bounded Blender workflow fixture with an authoritative scene.',
|
|
110
|
+
'Visual outcome: preserve a reviewable saved scene and immutable evidence.',
|
|
111
|
+
'Acceptance: every ticket, review, approval, and checkpoint gate must validate exact state.',
|
|
112
|
+
].join('\n');
|
|
113
|
+
|
|
114
|
+
const sections = (names) => names.flatMap((name) => [`## ${name}`, `Fixture ${name.toLowerCase()}.`, '']);
|
|
115
|
+
write(root, `${project}/README.md`, [
|
|
116
|
+
'# Fixture project',
|
|
117
|
+
'Status: Confirmed',
|
|
118
|
+
'Last updated: 2026-10-31',
|
|
119
|
+
'',
|
|
120
|
+
...sections(['Purpose', 'What this project is', 'Requirements', 'Visual direction', 'Deliverables', 'Project structure', 'Blender', 'Workflow', 'External assets', 'Goals', 'References', 'Open questions', 'Next steps']),
|
|
121
|
+
].join('\n'));
|
|
122
|
+
write(root, `${project}/Documents/REQUIREMENTS.md`, [
|
|
123
|
+
'# Requirements',
|
|
124
|
+
'Status: Confirmed',
|
|
125
|
+
'Defined: 2026-10-31',
|
|
126
|
+
'Last updated: 2026-10-31',
|
|
127
|
+
'Confirmed: 2026-10-31',
|
|
128
|
+
'Confirmation: Fixture requirements confirmed for adversarial workflow tests.',
|
|
129
|
+
'',
|
|
130
|
+
...sections(['Core value', 'Confirmed requirements', 'Constraints', 'Deferred', 'Out of scope', 'Open questions', 'Research notes', 'Change log']),
|
|
131
|
+
].join('\n'));
|
|
132
|
+
write(root, `${project}/Documents/WORKFLOW.md`, [
|
|
133
|
+
'# Workflow',
|
|
134
|
+
`Mode: ${workflow}`,
|
|
135
|
+
'Selected: 2026-10-31',
|
|
136
|
+
'Last changed: 2026-10-31',
|
|
137
|
+
'',
|
|
138
|
+
...sections(['Current mode', 'Rules', 'Change log']),
|
|
139
|
+
].join('\n'));
|
|
140
|
+
write(root, `${project}/Documents/REVIEW.md`, [
|
|
141
|
+
'# Review Mode',
|
|
142
|
+
`Mode: ${review}`,
|
|
143
|
+
'Selected: 2026-10-31',
|
|
144
|
+
'Last changed: 2026-10-31',
|
|
145
|
+
`Single grader: ${review === 'Single' ? 'visual' : '—'}`,
|
|
146
|
+
'',
|
|
147
|
+
...sections(['Modes', 'Change log']),
|
|
148
|
+
].join('\n'));
|
|
149
|
+
write(root, `${project}/Documents/ASSETS.md`, '# Fixture assets\n\n## Rules\n\nFixture asset rules.\n');
|
|
150
|
+
write(root, `${project}/Documents/DECISIONS.md`, '# Fixture decisions\n');
|
|
151
|
+
write(root, `${project}/Documents/References/SCENE-HEALTH.md`, '# Scene Health\n\n## Dimensions\n\nFixture dimensions.\n\n## Topology\n\nFixture topology.\n');
|
|
152
|
+
write(root, `${project}/Documents/TASKSTRACKER.md`, '# Task Tracker — Test\n\nStatus: Active\nLast updated: 2026-10-31\n\n## Tasks\n\n| Task | Kind | Change | Operation | Objects | Assigned | Child | State | Created | Finished | Result |\n|---|---|---|---|---|---|---|---|---|---|---|\n| — | — | — | — | — | — | — | — | — | — | — |\n');
|
|
153
|
+
write(root, `${project}/Documents/Goals/01-test-goal.md`, [
|
|
154
|
+
'# Goal: Fixture',
|
|
155
|
+
...['Mission', 'Project', 'Visual outcome', 'Scope', 'Deliverables', 'Acceptance', 'Workflow', 'Done'].flatMap((name) => [`## ${name}`, prompt, '']),
|
|
156
|
+
].join('\n'));
|
|
157
|
+
write(root, `${project}/Documents/Reviews/README.md`, '# Review rounds\n\n## Index\n\n| Round | UTC | Target | Mode | Graders | Verdict | Report |\n|---|---|---|---|---|---|---|\n| — | — | — | — | — | — | — |\n\n## Rules\n\nFixture rules.\n');
|
|
158
|
+
write(root, `${project}/Documents/Reviews/.gitignore`, '_evidence/\ntmp/\n');
|
|
159
|
+
write(root, `${project}/.gitignore`, 'logs/\nrenders/wip/**\n');
|
|
160
|
+
write(root, `${project}/.gitattributes`, '*.md text eol=crlf\n');
|
|
161
|
+
write(root, scene, Buffer.from('fixture blend bytes\n'));
|
|
162
|
+
write(root, `${project}/notes.txt`, 'initial notes\n');
|
|
163
|
+
|
|
164
|
+
return { project, goal, scene, sceneName, prompt: read(root, goal) };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function fixture({
|
|
168
|
+
workflow = 'Fully Autonomous',
|
|
169
|
+
review = 'Off',
|
|
170
|
+
commitInitial = true,
|
|
171
|
+
config = true,
|
|
172
|
+
configValue,
|
|
173
|
+
goalText,
|
|
174
|
+
} = {}) {
|
|
175
|
+
const root = fs.mkdtempSync(path.join(tempRoot, 'repo-'));
|
|
176
|
+
const stateRoot = fs.mkdtempSync(path.join(tempRoot, 'state-'));
|
|
177
|
+
const project = createProject(root, { workflow, review, goalText });
|
|
178
|
+
if (review !== 'Off' && config !== false) {
|
|
179
|
+
write(root, '.kilo/blender-project-harness/config.json', JSON.stringify(configValue ?? validGraderConfig(), null, 2));
|
|
180
|
+
} else if (configValue !== undefined) {
|
|
181
|
+
write(root, '.kilo/blender-project-harness/config.json', JSON.stringify(configValue, null, 2));
|
|
182
|
+
}
|
|
183
|
+
git(root, ['init', '--quiet']);
|
|
184
|
+
git(root, ['config', 'user.name', 'Harness Tests']);
|
|
185
|
+
git(root, ['config', 'user.email', 'harness-tests@example.invalid']);
|
|
186
|
+
if (commitInitial) commit(root, 'scaffold fixture');
|
|
187
|
+
const harness = new Harness(root, { stateRoot });
|
|
188
|
+
return {
|
|
189
|
+
root,
|
|
190
|
+
stateRoot,
|
|
191
|
+
harness,
|
|
192
|
+
...project,
|
|
193
|
+
cleanup() {
|
|
194
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
195
|
+
fs.rmSync(stateRoot, { recursive: true, force: true });
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function useFixture(t, options) {
|
|
201
|
+
const f = fixture(options);
|
|
202
|
+
t.after(() => f.cleanup());
|
|
203
|
+
return f;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function gate(fn, pattern = /Workflow gate:/) {
|
|
207
|
+
assert.throws(fn, pattern);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function registrationQuestion(prepared) {
|
|
211
|
+
return {
|
|
212
|
+
questions: [{
|
|
213
|
+
question: prepared.question,
|
|
214
|
+
options: prepared.labels.map((label) => ({ label })),
|
|
215
|
+
custom: false,
|
|
216
|
+
multiple: false,
|
|
217
|
+
}],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
test('registration accepts question args without custom or multiple flags', (t) => {
|
|
222
|
+
const f = useFixture(t);
|
|
223
|
+
const prepared = f.harness.prepare('orchestrator', { project: f.project, goal: f.goal.slice(f.project.length + 1), scene: f.sceneName });
|
|
224
|
+
f.harness.question('orchestrator', {
|
|
225
|
+
questions: [{ question: prepared.question, options: prepared.labels.map((label) => ({ label })) }],
|
|
226
|
+
}, [['Confirm registration']]);
|
|
227
|
+
assert.match(f.harness.command('orchestrator', 'status').next, /registered goal/);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('custom-answer questions do not register', (t) => {
|
|
231
|
+
const f = useFixture(t);
|
|
232
|
+
const prepared = f.harness.prepare('orchestrator', { project: f.project, goal: f.goal.slice(f.project.length + 1), scene: f.sceneName });
|
|
233
|
+
f.harness.question('orchestrator', {
|
|
234
|
+
questions: [{ question: prepared.question, options: prepared.labels.map((label) => ({ label })), custom: true }],
|
|
235
|
+
}, [['Confirm registration']]);
|
|
236
|
+
assert.equal(f.harness.command('orchestrator', 'status').project, null);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('status reports an unregistered phase instead of denying', (t) => {
|
|
240
|
+
const f = useFixture(t);
|
|
241
|
+
const status = f.harness.command('orchestrator', 'status');
|
|
242
|
+
assert.equal(status.project, null);
|
|
243
|
+
assert.match(status.next, /prepare/);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('unregistered status asks prepare when multiple goal documents exist', (t) => {
|
|
247
|
+
const f = useFixture(t);
|
|
248
|
+
write(f.root, `${f.project}/Documents/Goals/02-extra.md`, '# Goal: Extra\n\n## Mission\n\nFixture extra goal.\n');
|
|
249
|
+
commit(f.root, 'test: add extra goal', [f.project]);
|
|
250
|
+
const status = f.harness.command('orchestrator', 'status', { project: f.project });
|
|
251
|
+
assert.equal(status.nextStep.phase, 'prepare');
|
|
252
|
+
assert.match(status.nextStep.command, /01-test-goal/);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('prepare is idempotent and replaces its own expired challenge', (t) => {
|
|
256
|
+
const f = useFixture(t);
|
|
257
|
+
const args = { project: f.project, goal: f.goal.slice(f.project.length + 1), scene: f.sceneName };
|
|
258
|
+
const first = f.harness.prepare('orchestrator', args);
|
|
259
|
+
assert.equal(f.harness.prepare('orchestrator', args).question, first.question);
|
|
260
|
+
f.harness.transaction((s) => { s.challenge.created -= 600001; });
|
|
261
|
+
assert.notEqual(f.harness.prepare('orchestrator', args).question, first.question);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('pending registration does not lock out another session forever', (t) => {
|
|
265
|
+
const f = useFixture(t);
|
|
266
|
+
const args = { project: f.project, goal: f.goal.slice(f.project.length + 1), scene: f.sceneName };
|
|
267
|
+
f.harness.prepare('first', args);
|
|
268
|
+
gate(() => f.harness.prepare('second', args), /pending in another session/);
|
|
269
|
+
f.harness.transaction((s) => { s.challenge.created -= 600001; });
|
|
270
|
+
const prepared = f.harness.prepare('second', args);
|
|
271
|
+
f.harness.question('second', { questions: [{ question: prepared.question, options: prepared.labels.map((label) => ({ label })) }] }, [['Confirm registration']]);
|
|
272
|
+
assert.match(f.harness.command('second', 'status').next, /registered goal/);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('bounded changes track scenes and scene requirement mappings', (t) => {
|
|
276
|
+
const f = useFixture(t);
|
|
277
|
+
activate(f);
|
|
278
|
+
gate(() => begin(f, 'orchestrator', { scenes: [''], requirements: ['REQ-001'] }), /printable scene names/);
|
|
279
|
+
begin(f, 'orchestrator', {
|
|
280
|
+
scenes: ['SC_001', 'SC_002'],
|
|
281
|
+
requirements: ['REQ-001', 'REQ-002'],
|
|
282
|
+
sceneRequirements: { 'REQ-001': ['SC_001'], 'REQ-002': ['SC_001', 'SC_002'] },
|
|
283
|
+
});
|
|
284
|
+
let status = f.harness.command('orchestrator', 'status');
|
|
285
|
+
assert.deepEqual(status.change.scenes, ['SC_001', 'SC_002']);
|
|
286
|
+
assert.equal(status.change.primaryScene, 'SC_001');
|
|
287
|
+
assert.deepEqual(status.change.sceneRequirements['REQ-002'], ['SC_001', 'SC_002']);
|
|
288
|
+
f.harness.command('orchestrator', 'extend', { scenes: ['SC_003'], paths: ['Documents/DECISIONS.md'] });
|
|
289
|
+
status = f.harness.command('orchestrator', 'status');
|
|
290
|
+
assert.deepEqual(status.change.scenes, ['SC_001', 'SC_002', 'SC_003']);
|
|
291
|
+
gate(() => f.harness.command('orchestrator', 'extend', { scenes: [''], paths: ['Documents/DECISIONS.md'] }), /printable scene names/);
|
|
292
|
+
gate(() => f.harness.command('orchestrator', 'extend', { scenes: Array.from({ length: 33 }, (_, index) => `SC_${index}`), paths: ['Documents/DECISIONS.md'] }), /capped at 32|up to 32/);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('multi-scene seal requires an inspection for every declared scene', (t) => {
|
|
296
|
+
const f = useFixture(t);
|
|
297
|
+
activate(f);
|
|
298
|
+
begin(f, 'orchestrator', { scenes: ['SC_001', 'SC_002'], requirements: ['REQ-001'], sceneRequirements: { 'REQ-001': ['SC_001'] } });
|
|
299
|
+
const apply = issueAndConsume(f, 'orchestrator', 'apply');
|
|
300
|
+
f.harness.finish(apply.token, { ok: true, operationResults: [{ operation: 'create_mesh', ok: true }] });
|
|
301
|
+
f.harness.command('orchestrator', 'finalize');
|
|
302
|
+
const wipPath = `${f.project}/renders/wip/WIP-scenes.png`;
|
|
303
|
+
write(f.root, wipPath, Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
|
|
304
|
+
const wip = issueAndConsume(f, 'orchestrator', 'wip');
|
|
305
|
+
f.harness.finish(wip.token, { ok: true, renderPath: wipPath });
|
|
306
|
+
f.harness.inspect('orchestrator', path.resolve(f.root, wipPath), true);
|
|
307
|
+
const primary = `${f.project}/renders/final/R01-test.png`;
|
|
308
|
+
const secondary = `${f.project}/Documents/Reviews/R01-test/_evidence/scenes/SC_002/camera.png`;
|
|
309
|
+
write(f.root, primary, Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
|
|
310
|
+
write(f.root, secondary, Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
|
|
311
|
+
const evidence = Object.fromEntries([primary, secondary].map((name) => [name, f.harness.hash(name)]));
|
|
312
|
+
write(f.root, `${f.project}/Documents/Reviews/R01-test/manifest.json`, JSON.stringify({ round: 'R01-test', scene: f.scene, scenes: ['SC_001', 'SC_002'], evidence }, null, 2) + '\n');
|
|
313
|
+
const seal = issueAndConsume(f, 'orchestrator', 'seal');
|
|
314
|
+
f.harness.finish(seal.token, { ok: true, round: 'R01-test', renderPath: primary, evidence, renders: { SC_001: primary, SC_002: secondary }, primaryScene: 'SC_001', scenes: ['SC_001', 'SC_002'] });
|
|
315
|
+
let status = f.harness.command('orchestrator', 'status');
|
|
316
|
+
assert.equal(status.change.inspected, false);
|
|
317
|
+
assert.deepEqual(status.change.scenes, ['SC_001', 'SC_002']);
|
|
318
|
+
f.harness.inspect('orchestrator', path.resolve(f.root, primary), true);
|
|
319
|
+
assert.equal(f.harness.command('orchestrator', 'status').change.inspected, false);
|
|
320
|
+
f.harness.inspect('orchestrator', path.resolve(f.root, secondary), true);
|
|
321
|
+
status = f.harness.command('orchestrator', 'status');
|
|
322
|
+
assert.equal(status.change.inspected, true);
|
|
323
|
+
assert.equal(status.change.inspections.SC_002.hash, evidence[secondary]);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('bootstrap save closes with checkpoint and cannot be sealed or graded', (t) => {
|
|
327
|
+
const f = useFixture(t);
|
|
328
|
+
fs.rmSync(path.join(f.root, f.scene));
|
|
329
|
+
commit(f.root, 'test: remove fixture scene', [f.project]);
|
|
330
|
+
activate(f);
|
|
331
|
+
begin(f, 'orchestrator', { paths: ['Documents/DECISIONS.md'] });
|
|
332
|
+
assert.equal(f.harness.command('orchestrator', 'status').nextStep.phase, 'bootstrap-save');
|
|
333
|
+
gate(() => f.harness.issue('orchestrator', 'apply', {}), /bootstrap change accepts save only/);
|
|
334
|
+
gate(() => f.harness.issue('orchestrator', 'seal', {}), /bootstrap save/);
|
|
335
|
+
fs.writeFileSync(path.join(f.root, f.scene), 'saved scene bytes\n');
|
|
336
|
+
finishSave(f);
|
|
337
|
+
fs.appendFileSync(path.join(f.root, f.project, 'Documents', 'DECISIONS.md'), 'Bootstrap save.\n');
|
|
338
|
+
commit(f.root, 'feat: bootstrap save fixture scene', [f.project]);
|
|
339
|
+
const status = f.harness.command('orchestrator', 'checkpoint');
|
|
340
|
+
assert.equal(status.change, null);
|
|
341
|
+
assert.equal(f.harness.transaction((s) => s.projects[f.project].history.at(-1).verdict), 'BOOTSTRAP');
|
|
342
|
+
assert.doesNotThrow(() => begin(f));
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
function register(f, session = 'orchestrator') {
|
|
346
|
+
const prepared = f.harness.prepare(session, {
|
|
347
|
+
project: f.project,
|
|
348
|
+
goal: f.goal.slice(f.project.length + 1),
|
|
349
|
+
scene: f.sceneName,
|
|
350
|
+
});
|
|
351
|
+
f.harness.question(session, registrationQuestion(prepared), [['Confirm registration']]);
|
|
352
|
+
return prepared;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function activate(f, session = 'orchestrator') {
|
|
356
|
+
register(f, session);
|
|
357
|
+
f.harness.command(session, 'goal-active', { objective: f.prompt });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function begin(f, session = 'orchestrator', options = {}) {
|
|
361
|
+
const args = {
|
|
362
|
+
outcome: options.outcome ?? 'Update fixture scene',
|
|
363
|
+
objects: options.objects ?? ['FixtureCube'],
|
|
364
|
+
paths: options.paths ?? ['notes.txt'],
|
|
365
|
+
...(options.revision === undefined ? {} : { revision: options.revision }),
|
|
366
|
+
...(options.milestone === undefined ? {} : { milestone: options.milestone }),
|
|
367
|
+
...(options.requirements === undefined ? {} : { requirements: options.requirements }),
|
|
368
|
+
...(options.scenes === undefined ? {} : { scenes: options.scenes }),
|
|
369
|
+
...(options.sceneRequirements === undefined ? {} : { sceneRequirements: options.sceneRequirements }),
|
|
370
|
+
final: options.final ?? true,
|
|
371
|
+
};
|
|
372
|
+
return f.harness.command(session, 'begin', args);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function ticketPayload(f, extra = {}) {
|
|
376
|
+
return extra;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function issueAndConsume(f, session, operation, extra = {}) {
|
|
380
|
+
const payload = ticketPayload(f, extra);
|
|
381
|
+
const issued = f.harness.issue(session, operation, payload);
|
|
382
|
+
f.harness.authorize(session, issued.token, operation, payload);
|
|
383
|
+
const context = f.harness.consume(issued.token, operation, payload);
|
|
384
|
+
return { ...issued, payload, context };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function finishSave(f, session = 'orchestrator') {
|
|
388
|
+
const issued = issueAndConsume(f, session, 'save');
|
|
389
|
+
f.harness.finish(issued.token, { ok: true });
|
|
390
|
+
return issued;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function seal(f, session = 'orchestrator', { round = 'R01-20261031T110000Z', inspect = true } = {}) {
|
|
394
|
+
const change = f.harness.transaction((s) => s.projects[f.project].change);
|
|
395
|
+
if (change && change.operations === 0 && !change.failure && Date.now() <= change.deadline) {
|
|
396
|
+
const issued = issueAndConsume(f, session, 'apply', { sealFixture: true });
|
|
397
|
+
f.harness.finish(issued.token, { ok: true });
|
|
398
|
+
}
|
|
399
|
+
const current = f.harness.transaction((s) => s.projects[f.project].change);
|
|
400
|
+
const recovery = current && (current.failure || Date.now() > current.deadline);
|
|
401
|
+
if (current && !recovery && !current.readyToSeal) f.harness.command(session, 'finalize');
|
|
402
|
+
if (current && !recovery && (!current.wipLook || current.wipLook.operations !== current.operations)) {
|
|
403
|
+
const wipPath = `${f.project}/renders/wip/WIP-${round}.png`;
|
|
404
|
+
write(f.root, wipPath, Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
|
|
405
|
+
const wip = issueAndConsume(f, session, 'wip');
|
|
406
|
+
f.harness.finish(wip.token, { ok: true, renderPath: wipPath });
|
|
407
|
+
f.harness.inspect(session, path.resolve(f.root, wipPath), true);
|
|
408
|
+
}
|
|
409
|
+
const renderPath = `${f.project}/renders/final/${round}.png`;
|
|
410
|
+
const evidenceRoot = `${f.project}/Documents/Reviews/${round}/_evidence`;
|
|
411
|
+
const factsPath = `${evidenceRoot}/scene-facts.json`;
|
|
412
|
+
const evidenceNames = [
|
|
413
|
+
renderPath,
|
|
414
|
+
`${evidenceRoot}/camera.png`,
|
|
415
|
+
factsPath,
|
|
416
|
+
`${evidenceRoot}/scene.txt`,
|
|
417
|
+
];
|
|
418
|
+
write(f.root, renderPath, Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
|
|
419
|
+
write(f.root, `${evidenceRoot}/camera.png`, Buffer.from('camera evidence\n'));
|
|
420
|
+
write(f.root, factsPath, JSON.stringify({
|
|
421
|
+
name: 'Scene',
|
|
422
|
+
objects: [{
|
|
423
|
+
name: 'Cube',
|
|
424
|
+
type: 'MESH',
|
|
425
|
+
location: [0, 0, 1],
|
|
426
|
+
dimensions: [2, 2, 2],
|
|
427
|
+
mesh: {
|
|
428
|
+
vertices: 8,
|
|
429
|
+
edges: 12,
|
|
430
|
+
faces: 6,
|
|
431
|
+
ngons: 0,
|
|
432
|
+
non_manifold_edges: 0,
|
|
433
|
+
loose_vertices: 0,
|
|
434
|
+
degenerate_faces: 0,
|
|
435
|
+
},
|
|
436
|
+
}],
|
|
437
|
+
camera: 'Camera',
|
|
438
|
+
resolution: [1920, 1080],
|
|
439
|
+
engine: 'BLENDER_EEVEE',
|
|
440
|
+
}, null, 2) + '\n');
|
|
441
|
+
write(f.root, `${evidenceRoot}/scene.txt`, 'scene evidence\n');
|
|
442
|
+
const evidence = Object.fromEntries(evidenceNames.map((name) => [name, f.harness.hash(name)]));
|
|
443
|
+
const manifestPath = `${f.project}/Documents/Reviews/${round}/manifest.json`;
|
|
444
|
+
const assetCheck = f.harness.transaction((s) => s.projects[f.project].change?.assetCheckReceipt ?? null);
|
|
445
|
+
write(f.root, manifestPath, JSON.stringify({ round, scene: f.scene, asset_check: assetCheck, evidence, tasks: {} }, null, 2) + '\n');
|
|
446
|
+
const issued = issueAndConsume(f, session, 'seal');
|
|
447
|
+
const result = f.harness.finish(issued.token, { ok: true, round, renderPath, evidence });
|
|
448
|
+
const absoluteRender = path.resolve(f.root, renderPath);
|
|
449
|
+
if (inspect) f.harness.inspect(session, absoluteRender, true);
|
|
450
|
+
return { ...issued, result, round, renderPath, evidence, absoluteRender, factsPath, manifestPath };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function selfReview(f, session = 'orchestrator') {
|
|
454
|
+
return f.harness.command(session, 'self-review', { notes: 'Fixture inspection passed.' });
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function scopedCommit(f, message = 'change fixture') {
|
|
458
|
+
commit(f.root, message, [f.project]);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function reviewAll(f, session = 'orchestrator') {
|
|
462
|
+
const sealed = seal(f, session);
|
|
463
|
+
gate(() => selfReview(f, session));
|
|
464
|
+
gate(() => f.harness.command(session, 'review', { notes: 'Missing grader receipt must fail.' }));
|
|
465
|
+
const grader = f.harness.command(session, 'grader-ticket', { name: 'visual' });
|
|
466
|
+
gate(() => f.harness.command(session, 'grader-ticket', { name: 'visual' }));
|
|
467
|
+
gate(() => f.harness.collectGrader(session, 'unbound-child'));
|
|
468
|
+
f.harness.bindGrader(
|
|
469
|
+
session,
|
|
470
|
+
'child-grader',
|
|
471
|
+
`Review assignment ${grader.marker}`,
|
|
472
|
+
{ providerID: 'fixture-provider', modelID: 'fixture-model', variant: 'fixture-variant' },
|
|
473
|
+
);
|
|
474
|
+
gate(() => f.harness.bindGrader(
|
|
475
|
+
session,
|
|
476
|
+
'forged-child',
|
|
477
|
+
`Review assignment ${grader.marker}`,
|
|
478
|
+
{ providerID: 'fixture-provider', modelID: 'fixture-model', variant: 'fixture-variant' },
|
|
479
|
+
));
|
|
480
|
+
f.harness.graderStep('child-grader', 'read', {});
|
|
481
|
+
const reportText = [
|
|
482
|
+
'# Visual report',
|
|
483
|
+
'- Verdict: PASS',
|
|
484
|
+
`Scene hash: ${grader.sceneHash}`,
|
|
485
|
+
`Evidence key: ${grader.evidenceKey}`,
|
|
486
|
+
'| severity | finding |',
|
|
487
|
+
'| minor | none |',
|
|
488
|
+
'',
|
|
489
|
+
].join('\n');
|
|
490
|
+
write(f.root, grader.report, reportText);
|
|
491
|
+
f.harness.collectGrader(session, 'child-grader');
|
|
492
|
+
const reviewPath = `${f.project}/Documents/Reviews/${sealed.round}/REVIEW.md`;
|
|
493
|
+
write(f.root, reviewPath, [
|
|
494
|
+
'Verdict: PASS',
|
|
495
|
+
`Scene hash: ${grader.sceneHash}`,
|
|
496
|
+
'Finding dispositions: none',
|
|
497
|
+
'',
|
|
498
|
+
].join('\n'));
|
|
499
|
+
const status = f.harness.command(session, 'review', {
|
|
500
|
+
notes: 'All fixture findings disposed.',
|
|
501
|
+
reason: 'No corrections required; the sealed fixture satisfies the target.',
|
|
502
|
+
dispositions: 'No blocker or major finding remained.',
|
|
503
|
+
});
|
|
504
|
+
return { ...sealed, grader, reviewPath, status };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function simulationReport(ticket, verdict) {
|
|
508
|
+
return [
|
|
509
|
+
'# Grader report',
|
|
510
|
+
`- Verdict: ${verdict}`,
|
|
511
|
+
`Scene hash: ${ticket.sceneHash}`,
|
|
512
|
+
`Evidence key: ${ticket.evidenceKey}`,
|
|
513
|
+
...(verdict === 'PASS' ? [] : ['| severity | finding |', '| --- | --- |', '| major | synthetic finding |']),
|
|
514
|
+
'',
|
|
515
|
+
].join('\n');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function simulationGrader(f, name, verdict, { backup = false } = {}) {
|
|
519
|
+
const ticket = f.harness.command('orchestrator', 'grader-ticket', { name });
|
|
520
|
+
if (verdict === 'FAIL_START') {
|
|
521
|
+
f.harness.command('orchestrator', 'grader-fail', { name, reason: 'synthetic task start failure' });
|
|
522
|
+
return { ticket, child: null, result: null };
|
|
523
|
+
}
|
|
524
|
+
const child = `child-${name}-${crypto.randomUUID()}`;
|
|
525
|
+
const model = backup
|
|
526
|
+
? { providerID: 'backup-provider', modelID: 'backup-model', variant: 'backup-variant' }
|
|
527
|
+
: { providerID: 'fixture-provider', modelID: 'fixture-model', variant: 'fixture-variant' };
|
|
528
|
+
f.harness.bindGrader('orchestrator', child, `Review assignment ${ticket.marker}`, model);
|
|
529
|
+
if (verdict !== 'MISSING') write(f.root, ticket.report, simulationReport(ticket, verdict));
|
|
530
|
+
const result = f.harness.collectGrader('orchestrator', child);
|
|
531
|
+
return { ticket, child, result };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function simulationApply(f, index) {
|
|
535
|
+
const applied = issueAndConsume(f, 'orchestrator', 'apply', { simulation: index });
|
|
536
|
+
f.harness.finish(applied.token, { ok: true });
|
|
537
|
+
return applied;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function simulationAssetCheck(f) {
|
|
541
|
+
write(f.root, `${f.project}/notes.txt`, 'strict asset check fixture\n');
|
|
542
|
+
const applied = issueAndConsume(f, 'orchestrator', 'apply', { operation: 'check_assets', strict: true });
|
|
543
|
+
const receipt = {
|
|
544
|
+
ok: true,
|
|
545
|
+
operation: 'check_assets',
|
|
546
|
+
strict: true,
|
|
547
|
+
external: [],
|
|
548
|
+
missing: [],
|
|
549
|
+
packed: ['fixture.png'],
|
|
550
|
+
libraries: [],
|
|
551
|
+
};
|
|
552
|
+
f.harness.finish(applied.token, {
|
|
553
|
+
ok: true,
|
|
554
|
+
operationResults: [{ index: 1, operation: 'check_assets', ok: true, receipt }],
|
|
555
|
+
});
|
|
556
|
+
return applied;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function simulationReview(f, notes = 'Synthetic review completed.') {
|
|
560
|
+
return f.harness.command('orchestrator', 'review', {
|
|
561
|
+
notes,
|
|
562
|
+
reason: 'Synthetic findings were resolved or recorded against the sealed evidence.',
|
|
563
|
+
dispositions: 'No unrecorded design decision remained.',
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
test('prepare rejects an uncommitted scaffold and accepts Git-clean CRLF content', (t) => {
|
|
568
|
+
const missing = useFixture(t, { commitInitial: false });
|
|
569
|
+
gate(() => missing.harness.prepare('orchestrator', {
|
|
570
|
+
project: missing.project,
|
|
571
|
+
goal: missing.goal.slice(missing.project.length + 1),
|
|
572
|
+
scene: missing.sceneName,
|
|
573
|
+
}));
|
|
574
|
+
|
|
575
|
+
const f = useFixture(t);
|
|
576
|
+
const before = f.harness.snapshot(f.project);
|
|
577
|
+
const requirements = path.join(f.root, f.project, 'Documents', 'REQUIREMENTS.md');
|
|
578
|
+
fs.writeFileSync(requirements, fs.readFileSync(requirements, 'utf8').replace(/\r?\n/g, '\r\n'));
|
|
579
|
+
assert.deepEqual(f.harness.snapshot(f.project), before);
|
|
580
|
+
assert.equal(git(f.root, ['diff', '--numstat', '--', f.project]).trim(), '');
|
|
581
|
+
assert.doesNotThrow(() => register(f));
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
test('records reject invalid modes and grader configurations before registration', (t) => {
|
|
585
|
+
const cases = [
|
|
586
|
+
['requirements status', {}, (f) => write(f.root, `${f.project}/Documents/REQUIREMENTS.md`, 'Status: Pending\nConfirmed: 2026-10-31\nConfirmation: no\n')],
|
|
587
|
+
['workflow mode', {}, (f) => write(f.root, `${f.project}/Documents/WORKFLOW.md`, 'Mode: Unset\nSelected: 2026-10-31\n')],
|
|
588
|
+
['review mode', {}, (f) => write(f.root, `${f.project}/Documents/REVIEW.md`, 'Mode: Unset\nSelected: 2026-10-31\n')],
|
|
589
|
+
['missing grader config', { review: 'All', config: false }, null],
|
|
590
|
+
['disabled grader config', { review: 'All', configValue: validGraderConfig({ enabled: false }) }, null],
|
|
591
|
+
['legacy grader config', { review: 'All', configValue: { workers: {}, blender_graders: {} } }, null],
|
|
592
|
+
['invalid grader roster', { review: 'All', configValue: validGraderConfig({ graders: [] }) }, null],
|
|
593
|
+
['max rounds above four', { review: 'All', configValue: validGraderConfig({ max_rounds: 5 }) }, null],
|
|
594
|
+
];
|
|
595
|
+
for (const [name, options, mutate] of cases) {
|
|
596
|
+
const f = useFixture(t, options);
|
|
597
|
+
if (mutate) mutate(f);
|
|
598
|
+
gate(() => f.harness.prepare('orchestrator', {
|
|
599
|
+
project: f.project,
|
|
600
|
+
goal: f.goal.slice(f.project.length + 1),
|
|
601
|
+
scene: f.sceneName,
|
|
602
|
+
}));
|
|
603
|
+
assert.ok(name);
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
test('worker and grader backups validate and bind as the configured fallback', (t) => {
|
|
608
|
+
const config = validGraderConfig();
|
|
609
|
+
config.blender_subagents.workers = {
|
|
610
|
+
agent: 'blender-worker', provider: 'fixture-provider', model: 'fixture-model', variant: 'fixture-variant',
|
|
611
|
+
backup: { provider: 'backup-provider', model: 'backup-model', variant: 'backup-variant' },
|
|
612
|
+
max_minutes: 10, max_steps: 18, max_failed_attempts: 2, max_parallel: 1,
|
|
613
|
+
};
|
|
614
|
+
config.blender_subagents.graders.graders[0].backup = { provider: 'backup-provider', model: 'backup-model', variant: 'backup-variant' };
|
|
615
|
+
const f = useFixture(t, { review: 'All', configValue: config });
|
|
616
|
+
write(f.root, '.kilo/agent/blender-worker.md', '---\nsteps: 20\n---\n');
|
|
617
|
+
assert.equal(f.harness.workerConfig().backup.model, 'backup-model');
|
|
618
|
+
activate(f);
|
|
619
|
+
begin(f);
|
|
620
|
+
const sealed = seal(f);
|
|
621
|
+
const ticket = f.harness.command('orchestrator', 'grader-ticket', { name: 'visual' });
|
|
622
|
+
assert.equal(ticket.model.backup.model, 'backup-model');
|
|
623
|
+
assert.match(ticket.prompt, /backup backup-provider \/ backup-model \/ backup-variant/);
|
|
624
|
+
f.harness.bindGrader(
|
|
625
|
+
'orchestrator',
|
|
626
|
+
'child-backup',
|
|
627
|
+
`Review assignment ${ticket.marker}`,
|
|
628
|
+
{ providerID: 'backup-provider', modelID: 'backup-model', variant: 'backup-variant' },
|
|
629
|
+
);
|
|
630
|
+
assert.equal(f.harness.transaction((s) => s.projects[f.project].change.graders.visual.modelRole), 'backup');
|
|
631
|
+
assert.equal(f.harness.command('orchestrator', 'status').models.graders[0].backup.model, 'backup-model');
|
|
632
|
+
assert.equal(sealed.round, ticket.round);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
test('partial backup configuration is rejected before registration', (t) => {
|
|
636
|
+
const grader = validGraderConfig();
|
|
637
|
+
grader.blender_subagents.graders.graders[0].backup = { provider: 'backup-provider', model: 'backup-model' };
|
|
638
|
+
const graderFixture = useFixture(t, { review: 'All', configValue: grader });
|
|
639
|
+
gate(() => graderFixture.harness.prepare('orchestrator', {
|
|
640
|
+
project: graderFixture.project,
|
|
641
|
+
goal: graderFixture.goal.slice(graderFixture.project.length + 1),
|
|
642
|
+
scene: graderFixture.sceneName,
|
|
643
|
+
}), /backup|reasoning/);
|
|
644
|
+
|
|
645
|
+
const worker = validGraderConfig();
|
|
646
|
+
worker.blender_subagents.workers = {
|
|
647
|
+
agent: 'blender-worker', provider: 'fixture-provider', model: 'fixture-model', variant: 'fixture-variant',
|
|
648
|
+
backup: { provider: 'backup-provider', model: 'backup-model' },
|
|
649
|
+
max_minutes: 10, max_steps: 18, max_failed_attempts: 2, max_parallel: 1,
|
|
650
|
+
};
|
|
651
|
+
const workerFixture = useFixture(t, { review: 'Off', configValue: worker });
|
|
652
|
+
gate(() => workerFixture.harness.workerConfig(), /backup|reasoning/);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
test('registration accepts only exact runtime question and goal prompt', (t) => {
|
|
656
|
+
const f = useFixture(t);
|
|
657
|
+
const unregistered = (session = 'orchestrator') => assert.equal(f.harness.command(session, 'status').project, null);
|
|
658
|
+
gate(() => f.harness.command('orchestrator', 'goal-active', { objective: f.prompt }));
|
|
659
|
+
const prepared = f.harness.prepare('orchestrator', {
|
|
660
|
+
project: f.project,
|
|
661
|
+
goal: f.goal.slice(f.project.length + 1),
|
|
662
|
+
scene: f.sceneName,
|
|
663
|
+
});
|
|
664
|
+
f.harness.question('orchestrator', {
|
|
665
|
+
questions: [{ question: 'forged', options: [{ label: 'Confirm registration' }], custom: false, multiple: false }],
|
|
666
|
+
}, [['Confirm registration']]);
|
|
667
|
+
unregistered();
|
|
668
|
+
f.harness.question('orchestrator', registrationQuestion(prepared), [[{ label: 'Confirm registration' }]]);
|
|
669
|
+
unregistered();
|
|
670
|
+
const custom = registrationQuestion(prepared);
|
|
671
|
+
custom.questions[0].custom = true;
|
|
672
|
+
f.harness.question('orchestrator', custom, [['Confirm registration']]);
|
|
673
|
+
unregistered();
|
|
674
|
+
f.harness.question('other-orchestrator', registrationQuestion(prepared), [['Confirm registration']]);
|
|
675
|
+
unregistered('other-orchestrator');
|
|
676
|
+
f.harness.question('orchestrator', registrationQuestion(prepared), [['Confirm registration']]);
|
|
677
|
+
f.harness.command('orchestrator', 'goal-active', { objective: f.prompt });
|
|
678
|
+
f.harness.command('orchestrator', 'goal-active', { objective: f.prompt.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n') + '\r\n' });
|
|
679
|
+
gate(() => f.harness.command('orchestrator', 'goal-active', { objective: 'forged goal' }));
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
test('command and public method schemas reject malformed lifecycle input and path escapes', (t) => {
|
|
683
|
+
const f = useFixture(t);
|
|
684
|
+
gate(() => f.harness.prepare('orchestrator', { project: f.project, goal: f.goal.slice(f.project.length + 1), scene: 'wrong.txt' }));
|
|
685
|
+
gate(() => f.harness.prepare('orchestrator', { project: f.project, goal: f.goal.slice(f.project.length + 1), scene: f.sceneName, approved: true }));
|
|
686
|
+
gate(() => f.harness.prepare('orchestrator', { project: 'Blender/../escape', goal: 'Documents/Goals/01-test-goal.md', scene: f.sceneName }));
|
|
687
|
+
activate(f);
|
|
688
|
+
gate(() => f.harness.command('orchestrator', 'begin', { outcome: '', objects: ['FixtureCube'], paths: ['notes.txt'] }));
|
|
689
|
+
gate(() => f.harness.command('orchestrator', 'begin', { outcome: 'bad objects', objects: ['\u0000'], paths: ['notes.txt'] }));
|
|
690
|
+
gate(() => f.harness.command('orchestrator', 'begin', { outcome: 'bad paths', objects: ['FixtureCube'], paths: [] }));
|
|
691
|
+
gate(() => f.harness.command('orchestrator', 'begin', { outcome: 'escape', objects: ['FixtureCube'], paths: ['../outside.txt'] }));
|
|
692
|
+
gate(() => f.harness.command('orchestrator', 'begin', { outcome: 'drive', objects: ['FixtureCube'], paths: ['C:outside.txt'] }));
|
|
693
|
+
begin(f);
|
|
694
|
+
gate(() => f.harness.issue('orchestrator', 'unknown', ticketPayload(f)));
|
|
695
|
+
gate(() => f.harness.command('orchestrator', 'unknown-action'));
|
|
696
|
+
gate(() => f.harness.finish('forged-token', { ok: true }));
|
|
697
|
+
gate(() => f.harness.command('orchestrator', 'approval'));
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
test('tickets require hook authorization and reject forged, replayed, cross-session, and expired use', (t) => {
|
|
701
|
+
const f = useFixture(t);
|
|
702
|
+
activate(f);
|
|
703
|
+
begin(f);
|
|
704
|
+
const payload = ticketPayload(f, { operation: 'save' });
|
|
705
|
+
const issued = f.harness.issue('orchestrator', 'save', payload);
|
|
706
|
+
gate(() => f.harness.consume('forged-token', 'save', payload));
|
|
707
|
+
gate(() => f.harness.issue('other-session', 'save', payload));
|
|
708
|
+
gate(() => f.harness.issue('orchestrator', 'save', payload));
|
|
709
|
+
gate(() => f.harness.consume(issued.token, 'save', payload));
|
|
710
|
+
gate(() => f.harness.authorize('other-session', issued.token, 'save', payload));
|
|
711
|
+
gate(() => f.harness.authorize('orchestrator', issued.token, 'apply', payload));
|
|
712
|
+
gate(() => f.harness.authorize('orchestrator', issued.token, 'save', { ...payload, forged: true }));
|
|
713
|
+
f.harness.authorize('orchestrator', issued.token, 'save', payload);
|
|
714
|
+
gate(() => f.harness.authorize('orchestrator', issued.token, 'save', payload));
|
|
715
|
+
gate(() => f.harness.consume(issued.token, 'save', { ...payload, forged: true }));
|
|
716
|
+
f.harness.consume(issued.token, 'save', payload);
|
|
717
|
+
gate(() => f.harness.consume(issued.token, 'save', payload));
|
|
718
|
+
f.harness.finish(issued.token, { ok: true });
|
|
719
|
+
gate(() => f.harness.finish(issued.token, { ok: true }));
|
|
720
|
+
|
|
721
|
+
const expired = f.harness.issue('orchestrator', 'save', ticketPayload(f, { second: true }));
|
|
722
|
+
f.harness.transaction((state) => { state.tickets[expired.token].created = Date.now() - 600_001; });
|
|
723
|
+
gate(() => f.harness.authorize('orchestrator', expired.token, 'save', expired.payload));
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
test('deadline and twenty apply operations block edits but preserve recovery save and seal', (t) => {
|
|
727
|
+
const f = useFixture(t);
|
|
728
|
+
activate(f);
|
|
729
|
+
begin(f);
|
|
730
|
+
for (let index = 0; index < 20; index += 1) {
|
|
731
|
+
if (index > 0 && index % 3 === 0) {
|
|
732
|
+
const wipPath = `${f.project}/renders/wip/WIP-budget-${index}.png`;
|
|
733
|
+
write(f.root, wipPath, Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
|
|
734
|
+
const wip = issueAndConsume(f, 'orchestrator', 'wip');
|
|
735
|
+
f.harness.finish(wip.token, { ok: true, renderPath: wipPath });
|
|
736
|
+
f.harness.inspect('orchestrator', path.resolve(f.root, wipPath), true);
|
|
737
|
+
}
|
|
738
|
+
const issued = issueAndConsume(f, 'orchestrator', 'apply', { index });
|
|
739
|
+
f.harness.finish(issued.token, { ok: true });
|
|
740
|
+
}
|
|
741
|
+
assert.equal(f.harness.command('orchestrator', 'status').change.operations, 20);
|
|
742
|
+
gate(() => f.harness.issue('orchestrator', 'apply', ticketPayload(f, { index: 20 })));
|
|
743
|
+
finishSave(f);
|
|
744
|
+
const sealed = seal(f);
|
|
745
|
+
assert.equal(sealed.result.change.sealed, true);
|
|
746
|
+
|
|
747
|
+
const deadline = useFixture(t);
|
|
748
|
+
activate(deadline);
|
|
749
|
+
begin(deadline);
|
|
750
|
+
deadline.harness.transaction((state) => {
|
|
751
|
+
const project = Object.values(state.projects).find((entry) => entry.orchestrator === 'orchestrator');
|
|
752
|
+
project.change.deadline = Date.now() - 1;
|
|
753
|
+
});
|
|
754
|
+
gate(() => deadline.harness.issue('orchestrator', 'apply', ticketPayload(deadline, { late: true })));
|
|
755
|
+
finishSave(deadline);
|
|
756
|
+
assert.equal(deadline.harness.command('orchestrator', 'status').change.operations, 0);
|
|
757
|
+
assert.equal(seal(deadline).result.change.sealed, true);
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
test('recover rejects live writer and recovers expired unconsumed tickets', (t) => {
|
|
761
|
+
const f = useFixture(t);
|
|
762
|
+
activate(f);
|
|
763
|
+
begin(f);
|
|
764
|
+
const issued = issueAndConsume(f, 'orchestrator', 'apply', { recovery: true });
|
|
765
|
+
gate(() => f.harness.recover('orchestrator'));
|
|
766
|
+
f.harness.finish(issued.token, { ok: false, error: 'controlled upstream failure' });
|
|
767
|
+
const expired = f.harness.issue('orchestrator', 'save', {});
|
|
768
|
+
f.harness.transaction((state) => { state.tickets[expired.token].created = Date.now() - 600_001; });
|
|
769
|
+
const recovered = f.harness.recover('orchestrator');
|
|
770
|
+
assert.equal(recovered.change.sealed, false);
|
|
771
|
+
finishSave(f);
|
|
772
|
+
assert.equal(seal(f).result.change.sealed, true);
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
test('recover clears an expired unstarted ticket without closing edits', (t) => {
|
|
776
|
+
const f = useFixture(t);
|
|
777
|
+
activate(f);
|
|
778
|
+
begin(f);
|
|
779
|
+
const expired = f.harness.issue('orchestrator', 'apply', { retry: true });
|
|
780
|
+
f.harness.transaction((state) => { state.tickets[expired.token].created = Date.now() - 600_001; });
|
|
781
|
+
f.harness.recover('orchestrator');
|
|
782
|
+
const change = f.harness.transaction((state) => state.projects[f.project].change);
|
|
783
|
+
assert.equal(change.failure, undefined);
|
|
784
|
+
assert.equal(change.pending, undefined);
|
|
785
|
+
assert.ok(change.deadline > Date.now());
|
|
786
|
+
const retried = issueAndConsume(f, 'orchestrator', 'apply', { retried: true });
|
|
787
|
+
f.harness.finish(retried.token, { ok: true });
|
|
788
|
+
assert.equal(f.harness.command('orchestrator', 'status').change.operations, 1);
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
test('recover reopens a failed or expired reviewable change for repair', (t) => {
|
|
792
|
+
const f = useFixture(t);
|
|
793
|
+
activate(f);
|
|
794
|
+
begin(f);
|
|
795
|
+
f.harness.transaction((state) => {
|
|
796
|
+
const change = state.projects[f.project].change;
|
|
797
|
+
change.failure = 'Cannot render, no camera';
|
|
798
|
+
change.deadline = 0;
|
|
799
|
+
});
|
|
800
|
+
const failed = f.harness.command('orchestrator', 'status');
|
|
801
|
+
assert.equal(failed.nextStep.phase, 'recover');
|
|
802
|
+
assert.equal(failed.change.failure, true);
|
|
803
|
+
assert.equal(failed.change.appliesRemaining, 20);
|
|
804
|
+
const reopened = f.harness.recover('orchestrator', { reopen: true });
|
|
805
|
+
assert.equal(reopened.change.failure, false);
|
|
806
|
+
assert.equal(reopened.change.reopen, true);
|
|
807
|
+
assert.match(reopened.nextStep.command, /action:"ticket"/);
|
|
808
|
+
assert.ok(reopened.change.deadlineRemainingMs > 0);
|
|
809
|
+
const repaired = issueAndConsume(f, 'orchestrator', 'apply', { repair: true });
|
|
810
|
+
f.harness.finish(repaired.token, { ok: true });
|
|
811
|
+
const repairedStatus = f.harness.command('orchestrator', 'status');
|
|
812
|
+
assert.equal(repairedStatus.change.reopen, false);
|
|
813
|
+
assert.equal(repairedStatus.change.operations, 1);
|
|
814
|
+
assert.ok(repairedStatus.change.deadlineRemainingMs > 0);
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
test('task ledger records batched tickets and WIP commit preserves a failed change', (t) => {
|
|
818
|
+
const f = useFixture(t);
|
|
819
|
+
activate(f);
|
|
820
|
+
begin(f);
|
|
821
|
+
const issued = issueAndConsume(f, 'orchestrator', 'apply', { operations: [
|
|
822
|
+
{ operation: 'create_mesh', name: 'Cube', primitive: 'cube', location: [0, 0, 0], scale: [1, 1, 1] },
|
|
823
|
+
{ operation: 'create_mesh', name: 'Lamp', primitive: 'cylinder', location: [0, 0, 1], scale: [0.1, 0.1, 1] },
|
|
824
|
+
] });
|
|
825
|
+
f.harness.finish(issued.token, { ok: false, error: 'simulated batch failure', completed: 1 });
|
|
826
|
+
const ledger = read(f.root, `${f.project}/Documents/TASKSTRACKER.md`);
|
|
827
|
+
assert.match(ledger, /T001/);
|
|
828
|
+
assert.match(ledger, /apply x2/);
|
|
829
|
+
assert.match(ledger, /failed/);
|
|
830
|
+
assert.doesNotMatch(ledger, new RegExp(issued.token));
|
|
831
|
+
const failedStatus = f.harness.command('orchestrator', 'status');
|
|
832
|
+
assert.equal(failedStatus.change.saved, true);
|
|
833
|
+
assert.equal(failedStatus.change.sceneHash, f.harness.hash(f.scene));
|
|
834
|
+
const staged = f.harness.command('orchestrator', 'commit', { message: 'wip: preserve failed batch', mode: 'wip' });
|
|
835
|
+
assert.equal(staged.nextStep.phase, 'checkpoint');
|
|
836
|
+
const checkpointed = f.harness.command('orchestrator', 'checkpoint');
|
|
837
|
+
assert.equal(checkpointed.change, null);
|
|
838
|
+
assert.equal(checkpointed.nextStep.phase, 'begin');
|
|
839
|
+
const history = f.harness.transaction((state) => state.projects[f.project].history.at(-1));
|
|
840
|
+
assert.equal(history.verdict, 'WIP');
|
|
841
|
+
assert.doesNotThrow(() => begin(f));
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
test('apply commits skip ignored external asset binaries', (t) => {
|
|
845
|
+
const f = useFixture(t);
|
|
846
|
+
const asset = `${f.project}/assets/external/Poly-Haven/wood.jpg`;
|
|
847
|
+
write(f.root, asset, Buffer.from('ignored image bytes\n'));
|
|
848
|
+
write(f.root, `${f.project}/assets/external/Poly-Haven/SOURCE.md`, 'Source: Poly Haven\nRedistribution allowed: yes\n');
|
|
849
|
+
write(f.root, `${f.project}/assets/external/Poly-Haven/LICENSE.txt`, 'CC0-1.0\n');
|
|
850
|
+
fs.appendFileSync(path.join(f.root, f.project, '.gitignore'), 'assets/external/**/*.jpg\n');
|
|
851
|
+
commit(f.root, 'test: ignored external asset fixture', [f.project]);
|
|
852
|
+
activate(f);
|
|
853
|
+
begin(f, 'orchestrator', { paths: ['notes.txt', 'assets/external/Poly-Haven/wood.jpg'] });
|
|
854
|
+
write(f.root, `${f.project}/notes.txt`, 'texture batch changed notes\n');
|
|
855
|
+
const applied = issueAndConsume(f, 'orchestrator', 'apply', { simulation: 1 });
|
|
856
|
+
f.harness.finish(applied.token, { ok: true, operationResults: [{ operation: 'image_texture' }] });
|
|
857
|
+
assert.equal(f.harness.command('orchestrator', 'status').change.operations, 1);
|
|
858
|
+
const committed = git(f.root, ['show', '--name-only', '--pretty=format:', 'HEAD']).split(/\r?\n/).filter(Boolean);
|
|
859
|
+
assert.ok(!committed.includes(asset));
|
|
860
|
+
assert.ok(committed.includes(`${f.project}/notes.txt`));
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
test('WIP commit accepts a staged index and skips ignored external assets', (t) => {
|
|
864
|
+
const f = useFixture(t);
|
|
865
|
+
const asset = `${f.project}/assets/external/Poly-Haven/wood.jpg`;
|
|
866
|
+
write(f.root, asset, Buffer.from('ignored image bytes\n'));
|
|
867
|
+
write(f.root, `${f.project}/assets/external/Poly-Haven/SOURCE.md`, 'Source: Poly Haven\nRedistribution allowed: yes\n');
|
|
868
|
+
write(f.root, `${f.project}/assets/external/Poly-Haven/LICENSE.txt`, 'CC0-1.0\n');
|
|
869
|
+
fs.appendFileSync(path.join(f.root, f.project, '.gitignore'), 'assets/external/**/*.jpg\n');
|
|
870
|
+
commit(f.root, 'test: ignored external asset fixture', [f.project]);
|
|
871
|
+
activate(f);
|
|
872
|
+
begin(f, 'orchestrator', { paths: ['notes.txt', 'assets/external/Poly-Haven/wood.jpg'] });
|
|
873
|
+
write(f.root, `${f.project}/notes.txt`, 'staged before the apply failed\n');
|
|
874
|
+
git(f.root, ['add', '--', `${f.project}/notes.txt`]);
|
|
875
|
+
f.harness.transaction((state) => {
|
|
876
|
+
const change = state.projects[f.project].change;
|
|
877
|
+
change.failure = 'simulated staging failure';
|
|
878
|
+
change.deadline = 0;
|
|
879
|
+
});
|
|
880
|
+
const result = f.harness.command('orchestrator', 'commit', { message: 'wip: preserve interrupted apply', mode: 'wip' });
|
|
881
|
+
assert.equal(result.nextStep.phase, 'checkpoint');
|
|
882
|
+
const committed = git(f.root, ['show', '--name-only', '--pretty=format:', 'HEAD']).split(/\r?\n/).filter(Boolean);
|
|
883
|
+
assert.ok(!committed.includes(asset));
|
|
884
|
+
assert.ok(committed.includes(`${f.project}/notes.txt`));
|
|
885
|
+
const checkpointed = f.harness.command('orchestrator', 'checkpoint');
|
|
886
|
+
assert.equal(checkpointed.change, null);
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
test('failed change at the apply cap routes to WIP instead of an impossible recover', (t) => {
|
|
890
|
+
const f = useFixture(t);
|
|
891
|
+
activate(f);
|
|
892
|
+
begin(f);
|
|
893
|
+
f.harness.transaction((state) => {
|
|
894
|
+
const change = state.projects[f.project].change;
|
|
895
|
+
change.operations = 20;
|
|
896
|
+
change.failure = 'seal failed after cap';
|
|
897
|
+
change.deadline = 0;
|
|
898
|
+
});
|
|
899
|
+
const status = f.harness.command('orchestrator', 'status');
|
|
900
|
+
assert.equal(status.nextStep.phase, 'wip');
|
|
901
|
+
assert.match(status.nextStep.command, /action:"commit"/);
|
|
902
|
+
assert.match(status.nextStep.command, /wip/);
|
|
903
|
+
assert.throws(() => f.harness.recover('orchestrator', { reopen: true }), /budget is exhausted/);
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
test('failed apply closes edit route but preserves recovery save and seal', (t) => {
|
|
907
|
+
const f = useFixture(t);
|
|
908
|
+
activate(f);
|
|
909
|
+
begin(f);
|
|
910
|
+
const issued = issueAndConsume(f, 'orchestrator', 'apply', { failure: true });
|
|
911
|
+
f.harness.finish(issued.token, { ok: false, error: 'mock operation failed' });
|
|
912
|
+
gate(() => f.harness.issue('orchestrator', 'apply', { next: true }));
|
|
913
|
+
finishSave(f);
|
|
914
|
+
assert.equal(seal(f).result.change.sealed, true);
|
|
915
|
+
const status = f.harness.command('orchestrator', 'status');
|
|
916
|
+
assert.equal(status.nextStep.phase, 'self-review');
|
|
917
|
+
assert.equal(status.change.failure, false);
|
|
918
|
+
assert.equal(status.change.reopen, false);
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
test('checkpoint without an open change is rejected', (t) => {
|
|
922
|
+
const f = useFixture(t);
|
|
923
|
+
activate(f);
|
|
924
|
+
gate(() => f.harness.command('orchestrator', 'checkpoint'), /no open change/);
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
test('sealed render must be inspected before self-review can pass', (t) => {
|
|
928
|
+
const f = useFixture(t);
|
|
929
|
+
activate(f);
|
|
930
|
+
begin(f);
|
|
931
|
+
const sealed = seal(f, 'orchestrator', { inspect: false });
|
|
932
|
+
gate(() => selfReview(f));
|
|
933
|
+
f.harness.inspect('orchestrator', sealed.absoluteRender, false);
|
|
934
|
+
gate(() => selfReview(f));
|
|
935
|
+
f.harness.inspect('orchestrator', path.join(f.root, f.project, 'renders', 'final', 'wrong.png'), true);
|
|
936
|
+
gate(() => selfReview(f));
|
|
937
|
+
f.harness.inspect('orchestrator', sealed.absoluteRender, true);
|
|
938
|
+
selfReview(f);
|
|
939
|
+
assert.equal(f.harness.command('orchestrator', 'status').change.verdict, 'PASS');
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
test('stale scene, evidence, and specification freeze review state', (t) => {
|
|
943
|
+
for (const label of ['scene', 'evidence', 'specification', 'manifest']) {
|
|
944
|
+
const f = useFixture(t);
|
|
945
|
+
activate(f);
|
|
946
|
+
begin(f);
|
|
947
|
+
const sealed = seal(f);
|
|
948
|
+
if (label === 'scene') fs.appendFileSync(path.join(f.root, f.scene), 'changed');
|
|
949
|
+
else if (label === 'evidence') {
|
|
950
|
+
const evidencePath = Object.keys(sealed.evidence).find((name) => name.endsWith('scene.txt'));
|
|
951
|
+
fs.appendFileSync(path.join(f.root, evidencePath), 'changed');
|
|
952
|
+
} else if (label === 'specification') fs.appendFileSync(path.join(f.root, f.project, 'Documents', 'REQUIREMENTS.md'), 'changed\n');
|
|
953
|
+
else fs.appendFileSync(path.join(f.root, f.project, 'Documents', 'Reviews', sealed.round, 'manifest.json'), 'changed\n');
|
|
954
|
+
gate(() => selfReview(f));
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
test('partial approval ignores forged JSON and Pause, then accepts only actual labels', (t) => {
|
|
959
|
+
const f = useFixture(t, { workflow: 'Partially Autonomous' });
|
|
960
|
+
activate(f);
|
|
961
|
+
begin(f);
|
|
962
|
+
seal(f);
|
|
963
|
+
selfReview(f);
|
|
964
|
+
const approval = f.harness.command('orchestrator', 'approval');
|
|
965
|
+
f.harness.question('orchestrator', {
|
|
966
|
+
questions: [{ question: approval.question, options: approval.labels.map((label) => ({ label })), custom: false, multiple: false }],
|
|
967
|
+
}, [[{ label: 'Approve change' }]]);
|
|
968
|
+
assert.equal(f.harness.command('orchestrator', 'status').change.approved, false);
|
|
969
|
+
f.harness.question('orchestrator', {
|
|
970
|
+
questions: [{ question: approval.question, options: approval.labels.map((label) => ({ label })), custom: false, multiple: false }],
|
|
971
|
+
}, [['Pause']]);
|
|
972
|
+
assert.equal(f.harness.command('orchestrator', 'status').change.approved, false);
|
|
973
|
+
gate(() => f.harness.command('orchestrator', 'checkpoint'));
|
|
974
|
+
gate(() => begin(f));
|
|
975
|
+
const forged = registrationQuestion(approval);
|
|
976
|
+
forged.questions[0].options.push({ label: 'Forged approval' });
|
|
977
|
+
gate(() => f.harness.question('orchestrator', forged, [['Forged approval']]));
|
|
978
|
+
assert.equal(f.harness.command('orchestrator', 'status').change.approved, false);
|
|
979
|
+
f.harness.question('orchestrator', {
|
|
980
|
+
questions: [{ question: approval.question, options: approval.labels.map((label) => ({ label })), custom: false, multiple: false }],
|
|
981
|
+
}, [['Approve change']]);
|
|
982
|
+
assert.equal(f.harness.command('orchestrator', 'status').change.approved, true);
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
test('All review mode freezes grader evidence and rejects stale reports', (t) => {
|
|
986
|
+
const f = useFixture(t, { review: 'All' });
|
|
987
|
+
activate(f);
|
|
988
|
+
begin(f);
|
|
989
|
+
const reviewed = reviewAll(f);
|
|
990
|
+
assert.equal(reviewed.status.change.verdict, 'PASS');
|
|
991
|
+
assert.match(reviewed.grader.prompt, /HARNESS_GRADER:/);
|
|
992
|
+
assert.match(reviewed.grader.prompt, /Report path: .*REPORT\.md/);
|
|
993
|
+
assert.match(reviewed.grader.prompt, /Scene hash|saved scene SHA-256/);
|
|
994
|
+
const report = path.join(f.root, reviewed.grader.report);
|
|
995
|
+
fs.appendFileSync(report, 'tampered\n');
|
|
996
|
+
gate(() => f.harness.command('orchestrator', 'review', { notes: 'retry' }));
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
test('expired uncollected grader ticket can be reissued', (t) => {
|
|
1000
|
+
const f = useFixture(t, { review: 'All' });
|
|
1001
|
+
activate(f);
|
|
1002
|
+
begin(f);
|
|
1003
|
+
seal(f);
|
|
1004
|
+
const first = f.harness.command('orchestrator', 'grader-ticket', { name: 'visual' });
|
|
1005
|
+
f.harness.bindGrader(
|
|
1006
|
+
'orchestrator',
|
|
1007
|
+
'child-one',
|
|
1008
|
+
`Review assignment ${first.marker}`,
|
|
1009
|
+
{ providerID: 'fixture-provider', modelID: 'fixture-model', variant: 'fixture-variant' },
|
|
1010
|
+
);
|
|
1011
|
+
f.harness.transaction((s) => { s.projects[f.project].change.graders.visual.deadline = Date.now() - 1; });
|
|
1012
|
+
const retry = f.harness.command('orchestrator', 'grader-ticket', { name: 'visual' });
|
|
1013
|
+
assert.notEqual(retry.token, first.token);
|
|
1014
|
+
assert.equal(f.harness.command('orchestrator', 'status').change.graders.visual.attempt, 2);
|
|
1015
|
+
f.harness.transaction((s) => { s.projects[f.project].change.graders.visual.deadline = Date.now() - 1; });
|
|
1016
|
+
gate(() => f.harness.command('orchestrator', 'grader-ticket', { name: 'visual' }), /retry budget/);
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
test('review records an expired uncollected grader as BLOCKED', (t) => {
|
|
1020
|
+
const f = useFixture(t, { review: 'All' });
|
|
1021
|
+
activate(f);
|
|
1022
|
+
begin(f);
|
|
1023
|
+
const sealed = seal(f);
|
|
1024
|
+
const ticket = f.harness.command('orchestrator', 'grader-ticket', { name: 'visual' });
|
|
1025
|
+
f.harness.bindGrader(
|
|
1026
|
+
'orchestrator',
|
|
1027
|
+
'child-one',
|
|
1028
|
+
`Review assignment ${ticket.marker}`,
|
|
1029
|
+
{ providerID: 'fixture-provider', modelID: 'fixture-model', variant: 'fixture-variant' },
|
|
1030
|
+
);
|
|
1031
|
+
f.harness.transaction((s) => { s.projects[f.project].change.graders.visual.deadline = Date.now() - 1; });
|
|
1032
|
+
const sceneHash = f.harness.hash(f.scene);
|
|
1033
|
+
write(f.root, `${f.project}/Documents/Reviews/${sealed.round}/REVIEW.md`, [
|
|
1034
|
+
'Verdict: BLOCKED',
|
|
1035
|
+
`Scene hash: ${sceneHash}`,
|
|
1036
|
+
'Finding dispositions: grader expired without a report',
|
|
1037
|
+
'',
|
|
1038
|
+
].join('\n'));
|
|
1039
|
+
const reviewed = f.harness.command('orchestrator', 'review', {
|
|
1040
|
+
notes: 'Expired grader recorded BLOCKED.',
|
|
1041
|
+
reason: 'The expired grader prevents a pass; aggregate as blocked.',
|
|
1042
|
+
dispositions: 'No correction attempted; grader report is missing.',
|
|
1043
|
+
});
|
|
1044
|
+
assert.equal(reviewed.change.verdict, 'BLOCKED');
|
|
1045
|
+
assert.equal(reviewed.change.blockingKind, 'tooling');
|
|
1046
|
+
assert.match(reviewed.change.graders.visual.error, /deadline/);
|
|
1047
|
+
f.harness.command('orchestrator', 'grader-ticket', { name: 'visual' });
|
|
1048
|
+
assert.equal(f.harness.command('orchestrator', 'status').change.graders.visual.attempt, 2);
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
test('a sealed tooling block can be preserved, checkpointed, and paused', (t) => {
|
|
1052
|
+
const f = useFixture(t, { review: 'All' });
|
|
1053
|
+
activate(f);
|
|
1054
|
+
begin(f);
|
|
1055
|
+
seal(f);
|
|
1056
|
+
const runBlockedRound = () => {
|
|
1057
|
+
const ticket = f.harness.command('orchestrator', 'grader-ticket', { name: 'visual' });
|
|
1058
|
+
f.harness.bindGrader(
|
|
1059
|
+
'orchestrator',
|
|
1060
|
+
`child-${ticket.token}`,
|
|
1061
|
+
`Review assignment ${ticket.marker}`,
|
|
1062
|
+
{ providerID: 'fixture-provider', modelID: 'fixture-model', variant: 'fixture-variant' },
|
|
1063
|
+
);
|
|
1064
|
+
f.harness.transaction((s) => { s.projects[f.project].change.graders.visual.deadline = Date.now() - 1; });
|
|
1065
|
+
return f.harness.command('orchestrator', 'review', {
|
|
1066
|
+
notes: 'The grader did not produce a report.',
|
|
1067
|
+
reason: 'The provider remained unavailable, so the review is tooling-blocked.',
|
|
1068
|
+
dispositions: 'No design finding was assessed.',
|
|
1069
|
+
});
|
|
1070
|
+
};
|
|
1071
|
+
assert.equal(runBlockedRound().change.verdict, 'BLOCKED');
|
|
1072
|
+
assert.equal(f.harness.command('orchestrator', 'status').nextStep.phase, 'retry-grader');
|
|
1073
|
+
assert.equal(runBlockedRound().change.verdict, 'BLOCKED');
|
|
1074
|
+
assert.equal(f.harness.command('orchestrator', 'status').nextStep.phase, 'preserve');
|
|
1075
|
+
const preserved = f.harness.command('orchestrator', 'preserve', { reason: 'Preserve the sealed round for a later provider retry.' });
|
|
1076
|
+
assert.equal(preserved.change.wip, true);
|
|
1077
|
+
assert.equal(preserved.change.verdict, 'WIP');
|
|
1078
|
+
assert.match(preserved.change.preserved.reason, /later provider retry/);
|
|
1079
|
+
assert.equal(preserved.nextStep.phase, 'checkpoint');
|
|
1080
|
+
assert.equal(git(f.root, ['status', '--porcelain', '--', f.project]).trim(), '');
|
|
1081
|
+
f.harness.command('orchestrator', 'checkpoint');
|
|
1082
|
+
assert.equal(f.harness.transaction((s) => s.projects[f.project].history.at(-1).verdict), 'WIP');
|
|
1083
|
+
const paused = f.harness.command('orchestrator', 'pause', { reason: 'Provider unavailable; paused after preserving evidence.' });
|
|
1084
|
+
assert.equal(paused.goalActive, false);
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
test('shell approvals commit atomically in project or generic scope', (t) => {
|
|
1088
|
+
const record = () => ({
|
|
1089
|
+
id: crypto.randomUUID(),
|
|
1090
|
+
status: 'approved',
|
|
1091
|
+
session: 'orchestrator',
|
|
1092
|
+
command: 'npm run diagnostics',
|
|
1093
|
+
reasoning: 'Run the bounded workspace diagnostic before changing tooling.',
|
|
1094
|
+
cwd: 'D:\\Projects\\_Templates\\Blender',
|
|
1095
|
+
created: new Date().toISOString(),
|
|
1096
|
+
approvedAt: new Date().toISOString(),
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
const f = useFixture(t);
|
|
1100
|
+
activate(f);
|
|
1101
|
+
begin(f);
|
|
1102
|
+
const projectRecord = record();
|
|
1103
|
+
const project = f.harness.commitShellApproval('orchestrator', f.project, projectRecord);
|
|
1104
|
+
assert.equal(project.path, `Documents/Commands/${projectRecord.id}.md`);
|
|
1105
|
+
assert.ok(fs.existsSync(path.join(f.root, f.project, 'Documents', 'Commands', `${projectRecord.id}.md`)));
|
|
1106
|
+
assert.match(git(f.root, ['log', '-1', '--format=%s']), /shell approval/);
|
|
1107
|
+
seal(f);
|
|
1108
|
+
selfReview(f);
|
|
1109
|
+
f.harness.command('orchestrator', 'commit', { message: 'feat: fixture with command record' });
|
|
1110
|
+
assert.equal(f.harness.command('orchestrator', 'checkpoint').change, null);
|
|
1111
|
+
|
|
1112
|
+
const generic = useFixture(t);
|
|
1113
|
+
const genericRecord = record();
|
|
1114
|
+
const root = generic.harness.commitShellApproval('generic-session', null, genericRecord);
|
|
1115
|
+
assert.equal(root.path, `Commands/${genericRecord.id}.md`);
|
|
1116
|
+
assert.ok(fs.existsSync(path.join(generic.root, 'Commands', `${genericRecord.id}.md`)));
|
|
1117
|
+
assert.equal(git(generic.root, ['status', '--porcelain']).trim(), '');
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
test('grader tickets expose exact evidence paths and refuse tampered evidence', (t) => {
|
|
1121
|
+
const first = useFixture(t, { review: 'All' });
|
|
1122
|
+
activate(first);
|
|
1123
|
+
begin(first);
|
|
1124
|
+
const sealed = seal(first);
|
|
1125
|
+
const ticket = first.harness.command('orchestrator', 'grader-ticket', { name: 'visual' });
|
|
1126
|
+
assert.equal(ticket.evidenceFiles.length, 3);
|
|
1127
|
+
assert.match(ticket.prompt, /camera\.png/);
|
|
1128
|
+
assert.match(ticket.prompt, /scene\.txt/);
|
|
1129
|
+
assert.match(ticket.prompt, /manifest\.json/);
|
|
1130
|
+
assert.doesNotMatch(ticket.prompt, /Evidence: .*_evidence\n/);
|
|
1131
|
+
first.harness.bindGrader('orchestrator', 'child-evidence', `Review ${ticket.marker}`, { providerID: 'fixture-provider', modelID: 'fixture-model', variant: 'fixture-variant' });
|
|
1132
|
+
first.harness.transaction((s) => {
|
|
1133
|
+
s.projects[first.project].change.graders.visual.steps = 999;
|
|
1134
|
+
s.projects[first.project].change.graders.visual.deadline = Date.now() - 1;
|
|
1135
|
+
});
|
|
1136
|
+
assert.doesNotThrow(() => first.harness.graderStep('child-evidence', 'blender_document', { action: 'write' }));
|
|
1137
|
+
gate(() => first.harness.graderStep('child-evidence', 'read', {}), /budget/);
|
|
1138
|
+
|
|
1139
|
+
const second = useFixture(t, { review: 'All' });
|
|
1140
|
+
activate(second);
|
|
1141
|
+
begin(second);
|
|
1142
|
+
const sealedSecond = seal(second);
|
|
1143
|
+
fs.rmSync(path.join(second.root, second.project, 'Documents', 'Reviews', sealedSecond.round, '_evidence', 'camera.png'));
|
|
1144
|
+
gate(() => second.harness.command('orchestrator', 'grader-ticket', { name: 'visual' }), /missing path|frozen evidence/);
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
test('grader prompts distinguish partial milestone and final full-spec scope', (t) => {
|
|
1148
|
+
const partial = useFixture(t, { review: 'All' });
|
|
1149
|
+
activate(partial);
|
|
1150
|
+
begin(partial, 'orchestrator', {
|
|
1151
|
+
milestone: 'Fixture partial milestone',
|
|
1152
|
+
requirements: ['R1'],
|
|
1153
|
+
final: false,
|
|
1154
|
+
});
|
|
1155
|
+
seal(partial);
|
|
1156
|
+
const partialTicket = partial.harness.command('orchestrator', 'grader-ticket', { name: 'visual' });
|
|
1157
|
+
assert.match(partialTicket.prompt, /Final full-spec review: no/);
|
|
1158
|
+
assert.match(partialTicket.prompt, /Partial milestone review/);
|
|
1159
|
+
assert.match(partialTicket.prompt, /deferred, not a finding/);
|
|
1160
|
+
assert.match(partialTicket.prompt, /Report timing: .*write the assigned REPORT\.md immediately/i);
|
|
1161
|
+
|
|
1162
|
+
const final = useFixture(t, { review: 'All' });
|
|
1163
|
+
activate(final);
|
|
1164
|
+
begin(final);
|
|
1165
|
+
seal(final);
|
|
1166
|
+
const finalTicket = final.harness.command('orchestrator', 'grader-ticket', { name: 'visual' });
|
|
1167
|
+
assert.match(finalTicket.prompt, /Final full-spec review: yes/);
|
|
1168
|
+
assert.match(finalTicket.prompt, /Final full-spec review\. Grade the complete deliverable/);
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
test('missing grader report records BLOCKED and allows re-dispatch before review', (t) => {
|
|
1172
|
+
const f = useFixture(t, { review: 'All' });
|
|
1173
|
+
activate(f);
|
|
1174
|
+
begin(f);
|
|
1175
|
+
const sealed = seal(f);
|
|
1176
|
+
const first = f.harness.command('orchestrator', 'grader-ticket', { name: 'visual' });
|
|
1177
|
+
f.harness.bindGrader(
|
|
1178
|
+
'orchestrator',
|
|
1179
|
+
'child-one',
|
|
1180
|
+
`Review assignment ${first.marker}`,
|
|
1181
|
+
{ providerID: 'fixture-provider', modelID: 'fixture-model', variant: 'fixture-variant' },
|
|
1182
|
+
);
|
|
1183
|
+
const failed = f.harness.collectGrader('orchestrator', 'child-one');
|
|
1184
|
+
assert.equal(failed.change.graders.visual.verdict, 'BLOCKED');
|
|
1185
|
+
assert.match(failed.change.graders.visual.error, /missing/);
|
|
1186
|
+
|
|
1187
|
+
const retry = f.harness.command('orchestrator', 'grader-ticket', { name: 'visual' });
|
|
1188
|
+
assert.notEqual(retry.token, first.token);
|
|
1189
|
+
f.harness.bindGrader(
|
|
1190
|
+
'orchestrator',
|
|
1191
|
+
'child-two',
|
|
1192
|
+
`Review assignment ${retry.marker}`,
|
|
1193
|
+
{ providerID: 'fixture-provider', modelID: 'fixture-model', variant: 'fixture-variant' },
|
|
1194
|
+
);
|
|
1195
|
+
write(f.root, retry.report, [
|
|
1196
|
+
'# Visual report',
|
|
1197
|
+
'- Verdict: PASS',
|
|
1198
|
+
`Scene hash: ${retry.sceneHash}`,
|
|
1199
|
+
`Evidence key: ${retry.evidenceKey}`,
|
|
1200
|
+
'',
|
|
1201
|
+
].join('\n'));
|
|
1202
|
+
f.harness.collectGrader('orchestrator', 'child-two');
|
|
1203
|
+
write(f.root, `${f.project}/Documents/Reviews/${sealed.round}/REVIEW.md`, [
|
|
1204
|
+
'Verdict: PASS',
|
|
1205
|
+
`Scene hash: ${retry.sceneHash}`,
|
|
1206
|
+
'Finding dispositions: none',
|
|
1207
|
+
'',
|
|
1208
|
+
].join('\n'));
|
|
1209
|
+
const reviewed = f.harness.command('orchestrator', 'review', {
|
|
1210
|
+
notes: 'Retry passed.',
|
|
1211
|
+
reason: 'The replacement grader report passes after the missing-report failure.',
|
|
1212
|
+
dispositions: 'Missing report was retried once; no other findings.',
|
|
1213
|
+
});
|
|
1214
|
+
assert.equal(reviewed.change.verdict, 'PASS');
|
|
1215
|
+
gate(() => f.harness.command('orchestrator', 'grader-ticket', { name: 'visual' }));
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
test('retrying a collected BLOCKED report clears the stale evidence hash', (t) => {
|
|
1219
|
+
const f = useFixture(t, { review: 'All' });
|
|
1220
|
+
activate(f);
|
|
1221
|
+
begin(f);
|
|
1222
|
+
seal(f);
|
|
1223
|
+
const first = f.harness.command('orchestrator', 'grader-ticket', { name: 'visual' });
|
|
1224
|
+
f.harness.bindGrader(
|
|
1225
|
+
'orchestrator',
|
|
1226
|
+
'child-one',
|
|
1227
|
+
`Review assignment ${first.marker}`,
|
|
1228
|
+
{ providerID: 'fixture-provider', modelID: 'fixture-model', variant: 'fixture-variant' },
|
|
1229
|
+
);
|
|
1230
|
+
write(f.root, first.report, [
|
|
1231
|
+
'# Visual report',
|
|
1232
|
+
'- Verdict: BLOCKED',
|
|
1233
|
+
`Scene hash: ${first.sceneHash}`,
|
|
1234
|
+
`Evidence key: ${first.evidenceKey}`,
|
|
1235
|
+
'',
|
|
1236
|
+
].join('\n'));
|
|
1237
|
+
f.harness.collectGrader('orchestrator', 'child-one');
|
|
1238
|
+
assert.ok(f.harness.transaction((s) => s.projects[f.project].change.reports[first.report]));
|
|
1239
|
+
|
|
1240
|
+
const retry = f.harness.command('orchestrator', 'grader-ticket', { name: 'visual' });
|
|
1241
|
+
assert.equal(f.harness.transaction((s) => s.projects[f.project].change.graders.visual.attempt), 2);
|
|
1242
|
+
assert.equal(fs.existsSync(path.join(f.root, retry.report)), false);
|
|
1243
|
+
assert.equal(f.harness.transaction((s) => s.projects[f.project].change.reports[retry.report]), undefined);
|
|
1244
|
+
f.harness.transaction((s) => { s.projects[f.project].change.graders.visual.deadline = Date.now() - 1; });
|
|
1245
|
+
|
|
1246
|
+
const reviewed = f.harness.command('orchestrator', 'review', {
|
|
1247
|
+
notes: 'The retry produced no report.',
|
|
1248
|
+
reason: 'The grader is tooling-blocked and cannot provide a report.',
|
|
1249
|
+
dispositions: 'No design finding was assessed.',
|
|
1250
|
+
});
|
|
1251
|
+
assert.equal(reviewed.change.verdict, 'BLOCKED');
|
|
1252
|
+
assert.equal(f.harness.command('orchestrator', 'status').nextStep.phase, 'preserve');
|
|
1253
|
+
const preserved = f.harness.command('orchestrator', 'preserve', { reason: 'Preserve the blocked round for a later retry.' });
|
|
1254
|
+
assert.equal(preserved.change.verdict, 'WIP');
|
|
1255
|
+
assert.equal(preserved.nextStep.phase, 'checkpoint');
|
|
1256
|
+
assert.doesNotThrow(() => f.harness.command('orchestrator', 'checkpoint'));
|
|
1257
|
+
assert.equal(git(f.root, ['status', '--porcelain', '--', f.project]).trim(), '');
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
test('a failed grader start is recorded and exhausts the retry budget', (t) => {
|
|
1261
|
+
const f = useFixture(t, { review: 'All' });
|
|
1262
|
+
activate(f);
|
|
1263
|
+
begin(f);
|
|
1264
|
+
seal(f);
|
|
1265
|
+
const first = f.harness.command('orchestrator', 'grader-ticket', { name: 'visual' });
|
|
1266
|
+
f.harness.command('orchestrator', 'grader-fail', { name: 'visual', reason: 'task failed before child session' });
|
|
1267
|
+
assert.equal(f.harness.transaction((s) => s.projects[f.project].change.graders.visual.verdict), 'BLOCKED');
|
|
1268
|
+
assert.equal(f.harness.command('orchestrator', 'status').nextStep.phase, 'review');
|
|
1269
|
+
const firstReview = f.harness.command('orchestrator', 'review', {
|
|
1270
|
+
notes: 'The first dispatch produced no child.',
|
|
1271
|
+
reason: 'The grader task failed before it could report.',
|
|
1272
|
+
dispositions: 'No design finding was assessed.',
|
|
1273
|
+
});
|
|
1274
|
+
assert.equal(firstReview.change.verdict, 'BLOCKED');
|
|
1275
|
+
assert.equal(f.harness.command('orchestrator', 'status').nextStep.phase, 'retry-grader');
|
|
1276
|
+
|
|
1277
|
+
const retry = f.harness.command('orchestrator', 'grader-ticket', { name: 'visual' });
|
|
1278
|
+
assert.notEqual(retry.token, first.token);
|
|
1279
|
+
assert.equal(f.harness.transaction((s) => s.projects[f.project].change.graders.visual.attempt), 2);
|
|
1280
|
+
f.harness.command('orchestrator', 'grader-fail', { name: 'visual', reason: 'retry failed before child session' });
|
|
1281
|
+
assert.equal(f.harness.command('orchestrator', 'status').nextStep.phase, 'review');
|
|
1282
|
+
const reviewed = f.harness.command('orchestrator', 'review', {
|
|
1283
|
+
notes: 'The retry also produced no child.',
|
|
1284
|
+
reason: 'Both grader attempts failed before binding a child.',
|
|
1285
|
+
dispositions: 'No design finding was assessed.',
|
|
1286
|
+
});
|
|
1287
|
+
assert.equal(reviewed.change.verdict, 'BLOCKED');
|
|
1288
|
+
assert.equal(f.harness.command('orchestrator', 'status').nextStep.phase, 'preserve');
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
test('review registration record is frozen after registration', (t) => {
|
|
1292
|
+
const f = useFixture(t);
|
|
1293
|
+
activate(f);
|
|
1294
|
+
begin(f);
|
|
1295
|
+
seal(f);
|
|
1296
|
+
selfReview(f);
|
|
1297
|
+
fs.appendFileSync(path.join(f.root, f.project, 'Documents', 'REVIEW.md'), '\nReview notes updated.\n');
|
|
1298
|
+
gate(() => f.harness.command('orchestrator', 'checkpoint'), /spec, modes|re-registration/);
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
test('escalation can extend the review cap before re-registration', (t) => {
|
|
1302
|
+
const f = useFixture(t, { review: 'All' });
|
|
1303
|
+
activate(f);
|
|
1304
|
+
begin(f);
|
|
1305
|
+
reviewAll(f);
|
|
1306
|
+
f.harness.command('orchestrator', 'checkpoint');
|
|
1307
|
+
f.harness.command('orchestrator', 'escalate', { reason: 'Approved extra correction rounds.', max_rounds: 4 });
|
|
1308
|
+
register(f);
|
|
1309
|
+
assert.equal(f.harness.transaction((s) => s.projects[f.project].records.maxRounds), 4);
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
test('bootstrap documents and commits are harness-owned before registration', (t) => {
|
|
1313
|
+
const f = useFixture(t, { commitInitial: false });
|
|
1314
|
+
fs.rmSync(path.join(f.root, f.scene));
|
|
1315
|
+
fs.rmSync(path.join(f.root, f.project, 'notes.txt'));
|
|
1316
|
+
const status = f.harness.command('orchestrator', 'status', { project: f.project });
|
|
1317
|
+
assert.match(status.next, /bootstrap-commit/);
|
|
1318
|
+
assert.match(status.nextStep.command, /bootstrap-commit/);
|
|
1319
|
+
gate(() => f.harness.bootstrapDocument('orchestrator', { project: f.project, path: 'Documents/References/note.md', content: 'No heading\n' }));
|
|
1320
|
+
assert.doesNotThrow(() => f.harness.bootstrapDocument('orchestrator', {
|
|
1321
|
+
project: f.project,
|
|
1322
|
+
path: 'Documents/References/note.md',
|
|
1323
|
+
content: '# Reference note\n',
|
|
1324
|
+
}));
|
|
1325
|
+
const committed = f.harness.bootstrapCommit('orchestrator', { project: f.project, message: 'docs: scaffold fixture' });
|
|
1326
|
+
assert.equal(committed.next.includes('prepare'), true);
|
|
1327
|
+
assert.match(f.harness.command('orchestrator', 'status', { project: f.project }).nextStep.command, /prepare/);
|
|
1328
|
+
assert.doesNotThrow(() => register(f));
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
test('document read returns structure and section content without raw full-file reads', (t) => {
|
|
1332
|
+
const f = useFixture(t);
|
|
1333
|
+
activate(f);
|
|
1334
|
+
const read = f.harness.documentRead('orchestrator', { path: 'Documents/REQUIREMENTS.md' });
|
|
1335
|
+
assert.equal(read.title, 'Requirements');
|
|
1336
|
+
assert.equal(read.content, undefined);
|
|
1337
|
+
assert.equal(read.valid, true);
|
|
1338
|
+
assert.equal(read.fields.Status, 'Confirmed');
|
|
1339
|
+
assert.deepEqual(read.sections.slice(0, 3).map((section) => section.heading), ['Core value', 'Confirmed requirements', 'Constraints']);
|
|
1340
|
+
const section = f.harness.documentRead('orchestrator', { path: 'Documents/REQUIREMENTS.md', section: 'Confirmed requirements' });
|
|
1341
|
+
assert.match(section.section.content, /Fixture confirmed requirements/);
|
|
1342
|
+
const absolute = f.harness.documentRead('orchestrator', { path: path.join(f.root, f.project, 'Documents', 'WORKFLOW.md') });
|
|
1343
|
+
assert.equal(absolute.title, 'Workflow');
|
|
1344
|
+
gate(() => f.harness.documentRead('orchestrator', { path: 'Documents/REQUIREMENTS.md', section: 'Missing' }), /available/);
|
|
1345
|
+
gate(() => f.harness.documentRead('orchestrator', { path: 'scene.blend' }), /markdown/);
|
|
1346
|
+
write(f.root, `${f.project}/Documents/REQUIREMENTS.md`, '# Requirements\n\nStatus: Confirmed\n');
|
|
1347
|
+
const invalid = f.harness.documentRead('orchestrator', { path: 'Documents/REQUIREMENTS.md' });
|
|
1348
|
+
assert.equal(invalid.valid, false);
|
|
1349
|
+
assert.match(invalid.diagnostics[0], /field|section/);
|
|
1350
|
+
assert.match(invalid.next, /capabilities\.documentFormats/);
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
test('document read works before registration with an explicit project', (t) => {
|
|
1354
|
+
const f = useFixture(t, { commitInitial: false });
|
|
1355
|
+
const read = f.harness.documentRead('unregistered', { project: f.project, path: 'README.md' });
|
|
1356
|
+
assert.equal(read.title, 'Fixture project');
|
|
1357
|
+
assert.equal(read.content, undefined);
|
|
1358
|
+
assert.ok(read.sections.some((section) => section.heading === 'Purpose'));
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
test('grader document writes are limited to the assigned report format and path', (t) => {
|
|
1362
|
+
const f = useFixture(t, { review: 'All' });
|
|
1363
|
+
activate(f);
|
|
1364
|
+
begin(f);
|
|
1365
|
+
seal(f);
|
|
1366
|
+
const ticket = f.harness.command('orchestrator', 'grader-ticket', { name: 'visual' });
|
|
1367
|
+
const report = path.join(f.root, ticket.report);
|
|
1368
|
+
f.harness.transaction((state) => { state.projects[f.project].change.graders.visual.child = 'child-grader'; });
|
|
1369
|
+
const content = [
|
|
1370
|
+
'# Grader Report — visual',
|
|
1371
|
+
'',
|
|
1372
|
+
`- Evidence key: ${ticket.evidenceKey}`,
|
|
1373
|
+
`- Target: ${ticket.sceneHash}`,
|
|
1374
|
+
'- Verdict: PASS',
|
|
1375
|
+
'',
|
|
1376
|
+
'## Findings',
|
|
1377
|
+
'',
|
|
1378
|
+
'none',
|
|
1379
|
+
'',
|
|
1380
|
+
].join('\n');
|
|
1381
|
+
f.harness.documentWrite('child-grader', { path: report, content });
|
|
1382
|
+
assert.ok(fs.existsSync(path.join(f.root, ticket.report)));
|
|
1383
|
+
gate(() => f.harness.documentWrite('child-grader', { path: 'Documents/REQUIREMENTS.md', content: '# Required\n' }), /assigned report/);
|
|
1384
|
+
gate(() => f.harness.documentWrite('child-grader', { path: report, content: '# Missing verdict\n' }), /Verdict/);
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
test('a grader may write its report after the deadline but not after a verdict', (t) => {
|
|
1388
|
+
const f = useFixture(t, { review: 'All' });
|
|
1389
|
+
activate(f);
|
|
1390
|
+
begin(f);
|
|
1391
|
+
seal(f);
|
|
1392
|
+
const ticket = f.harness.command('orchestrator', 'grader-ticket', { name: 'visual' });
|
|
1393
|
+
const report = path.join(f.root, ticket.report);
|
|
1394
|
+
f.harness.transaction((state) => {
|
|
1395
|
+
const grader = state.projects[f.project].change.graders.visual;
|
|
1396
|
+
grader.child = 'child-late-report';
|
|
1397
|
+
grader.steps = 999;
|
|
1398
|
+
grader.deadline = Date.now() - 1;
|
|
1399
|
+
});
|
|
1400
|
+
const content = [
|
|
1401
|
+
'# Grader Report — visual',
|
|
1402
|
+
'',
|
|
1403
|
+
`- Evidence key: ${ticket.evidenceKey}`,
|
|
1404
|
+
`- Target: ${ticket.sceneHash}`,
|
|
1405
|
+
'- Verdict: PASS',
|
|
1406
|
+
'',
|
|
1407
|
+
'## Findings',
|
|
1408
|
+
'',
|
|
1409
|
+
'none',
|
|
1410
|
+
'',
|
|
1411
|
+
].join('\n');
|
|
1412
|
+
f.harness.documentWrite('child-late-report', { path: report, content });
|
|
1413
|
+
assert.ok(fs.existsSync(path.join(f.root, ticket.report)));
|
|
1414
|
+
f.harness.transaction((state) => { state.projects[f.project].change.graders.visual.verdict = 'PASS'; });
|
|
1415
|
+
gate(() => f.harness.documentWrite('child-late-report', { path: report, content }), /already finalized/);
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
test('graders may read their own prior report but not peer reports', (t) => {
|
|
1419
|
+
const f = useFixture(t, { review: 'All' });
|
|
1420
|
+
activate(f);
|
|
1421
|
+
begin(f);
|
|
1422
|
+
seal(f);
|
|
1423
|
+
const ticket = f.harness.command('orchestrator', 'grader-ticket', { name: 'visual' });
|
|
1424
|
+
f.harness.bindGrader(
|
|
1425
|
+
'orchestrator',
|
|
1426
|
+
'child-grader',
|
|
1427
|
+
`Review assignment ${ticket.marker}`,
|
|
1428
|
+
{ providerID: 'fixture-provider', modelID: 'fixture-model', variant: 'fixture-variant' },
|
|
1429
|
+
);
|
|
1430
|
+
const prior = 'R00-20261031T100000Z';
|
|
1431
|
+
write(f.root, `${f.project}/Documents/Reviews/${prior}/visual/REPORT.md`, '# Visual prior report\n');
|
|
1432
|
+
write(f.root, `${f.project}/Documents/Reviews/${prior}/technical/REPORT.md`, '# Technical prior report\n');
|
|
1433
|
+
const own = f.harness.documentRead('child-grader', { path: `Documents/Reviews/${prior}/visual/REPORT.md` });
|
|
1434
|
+
assert.equal(own.path, `Documents/Reviews/${prior}/visual/REPORT.md`);
|
|
1435
|
+
gate(() => f.harness.documentRead('child-grader', { path: `Documents/Reviews/${prior}/technical/REPORT.md` }), /outside assigned/);
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
test('seal requires at least one apply operation', (t) => {
|
|
1439
|
+
const f = useFixture(t);
|
|
1440
|
+
activate(f);
|
|
1441
|
+
begin(f);
|
|
1442
|
+
gate(() => f.harness.issue('orchestrator', 'seal', {}), /at least one apply/);
|
|
1443
|
+
const applied = issueAndConsume(f, 'orchestrator', 'apply', { seal: true });
|
|
1444
|
+
f.harness.finish(applied.token, { ok: true });
|
|
1445
|
+
assert.doesNotThrow(() => seal(f, 'orchestrator', { inspect: false }));
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
test('bootstrap commit rejects scene and binary deliverables', (t) => {
|
|
1449
|
+
const f = useFixture(t, { commitInitial: false });
|
|
1450
|
+
gate(() => f.harness.bootstrapCommit('orchestrator', { project: f.project, message: 'docs: invalid scaffold' }));
|
|
1451
|
+
assert.doesNotThrow(() => {
|
|
1452
|
+
fs.rmSync(path.join(f.root, f.scene));
|
|
1453
|
+
fs.rmSync(path.join(f.root, f.project, 'notes.txt'));
|
|
1454
|
+
f.harness.bootstrapCommit('orchestrator', { project: f.project, message: 'docs: valid scaffold' });
|
|
1455
|
+
});
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
test('status returns an exact next call for every registered phase', (t) => {
|
|
1459
|
+
const f = useFixture(t);
|
|
1460
|
+
activate(f);
|
|
1461
|
+
assert.match(f.harness.command('orchestrator', 'status').nextStep.command, /action:"begin"/);
|
|
1462
|
+
begin(f);
|
|
1463
|
+
assert.match(f.harness.command('orchestrator', 'status').nextStep.command, /action:"ticket"/);
|
|
1464
|
+
const issued = f.harness.issue('orchestrator', 'apply', { retry: true });
|
|
1465
|
+
const pending = f.harness.command('orchestrator', 'status');
|
|
1466
|
+
assert.equal(pending.nextStep.actor, 'blender-worker');
|
|
1467
|
+
assert.match(pending.nextStep.command, new RegExp(issued.token));
|
|
1468
|
+
f.harness.authorize('orchestrator', issued.token, 'apply', { retry: true });
|
|
1469
|
+
f.harness.consume(issued.token, 'apply', { retry: true });
|
|
1470
|
+
f.harness.finish(issued.token, { ok: true });
|
|
1471
|
+
assert.match(f.harness.command('orchestrator', 'status').nextStep.command, /action:"ticket"/);
|
|
1472
|
+
const sealed = seal(f, 'orchestrator', { inspect: false });
|
|
1473
|
+
assert.match(f.harness.command('orchestrator', 'status').nextStep.command, /blender_look/);
|
|
1474
|
+
f.harness.inspect('orchestrator', sealed.absoluteRender, true);
|
|
1475
|
+
selfReview(f);
|
|
1476
|
+
assert.match(f.harness.command('orchestrator', 'status').nextStep.command, /action:"commit"/);
|
|
1477
|
+
f.harness.command('orchestrator', 'commit', { message: 'feat: fixture change' });
|
|
1478
|
+
assert.match(f.harness.command('orchestrator', 'status').nextStep.command, /action:"checkpoint"/);
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
test('harness document action creates declared markdown only', (t) => {
|
|
1482
|
+
const f = useFixture(t);
|
|
1483
|
+
activate(f);
|
|
1484
|
+
begin(f, 'orchestrator', { paths: ['Documents/DECISIONS.md', 'Documents/NEW.md'] });
|
|
1485
|
+
f.harness.command('orchestrator', 'document', { path: 'Documents/NEW.md', content: '# New document\n' });
|
|
1486
|
+
assert.equal(read(f.root, `${f.project}/Documents/NEW.md`), '# New document\n');
|
|
1487
|
+
gate(() => f.harness.command('orchestrator', 'document', { path: 'Documents/Reviews/R01/REVIEW.md', content: 'manual' }));
|
|
1488
|
+
gate(() => f.harness.command('orchestrator', 'document', { path: 'Documents/UNDECLARED.md', content: 'outside' }));
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
test('document-only changes commit without sealing and close at checkpoint', (t) => {
|
|
1492
|
+
const f = useFixture(t);
|
|
1493
|
+
activate(f);
|
|
1494
|
+
begin(f, 'orchestrator', { paths: ['README.md', 'Documents/DECISIONS.md'] });
|
|
1495
|
+
const status = f.harness.command('orchestrator', 'status');
|
|
1496
|
+
assert.equal(status.nextStep.phase, 'commit');
|
|
1497
|
+
gate(() => f.harness.issue('orchestrator', 'apply', {}), /document-only/);
|
|
1498
|
+
gate(() => f.harness.issue('orchestrator', 'seal', {}), /document-only/);
|
|
1499
|
+
fs.appendFileSync(path.join(f.root, f.project, 'README.md'), 'Document-only change.\n');
|
|
1500
|
+
const committed = f.harness.command('orchestrator', 'commit', { message: 'docs: update fixture readme' });
|
|
1501
|
+
assert.equal(committed.nextStep.phase, 'checkpoint');
|
|
1502
|
+
const checkpointed = f.harness.command('orchestrator', 'checkpoint');
|
|
1503
|
+
assert.equal(checkpointed.change, null);
|
|
1504
|
+
assert.equal(checkpointed.nextStep.phase, 'begin');
|
|
1505
|
+
assert.equal(f.harness.transaction((state) => state.projects[f.project].history.at(-1).verdict), 'DOCUMENT');
|
|
1506
|
+
assert.doesNotThrow(() => begin(f));
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
test('document-only commit stages a declared deletion', (t) => {
|
|
1510
|
+
const f = useFixture(t);
|
|
1511
|
+
write(f.root, `${f.project}/Documents/References/extra.md`, '# Extra reference\n');
|
|
1512
|
+
commit(f.root, 'test: add removable reference', [f.project]);
|
|
1513
|
+
activate(f);
|
|
1514
|
+
begin(f, 'orchestrator', { paths: ['Documents/References/extra.md'] });
|
|
1515
|
+
fs.rmSync(path.join(f.root, f.project, 'Documents', 'References', 'extra.md'));
|
|
1516
|
+
const committed = f.harness.command('orchestrator', 'commit', { message: 'docs: remove fixture reference' });
|
|
1517
|
+
assert.equal(committed.nextStep.phase, 'checkpoint');
|
|
1518
|
+
assert.equal(f.harness.command('orchestrator', 'checkpoint').change, null);
|
|
1519
|
+
});
|
|
1520
|
+
|
|
1521
|
+
test('document-only checkpoint preserves a prior PASS for completion', (t) => {
|
|
1522
|
+
const f = useFixture(t);
|
|
1523
|
+
activate(f);
|
|
1524
|
+
begin(f);
|
|
1525
|
+
seal(f);
|
|
1526
|
+
selfReview(f);
|
|
1527
|
+
f.harness.command('orchestrator', 'commit', { message: 'feat: fixture pass' });
|
|
1528
|
+
f.harness.command('orchestrator', 'checkpoint');
|
|
1529
|
+
begin(f, 'orchestrator', { paths: ['README.md'] });
|
|
1530
|
+
fs.appendFileSync(path.join(f.root, f.project, 'README.md'), 'Document-only completion note.\n');
|
|
1531
|
+
f.harness.command('orchestrator', 'commit', { message: 'docs: fixture completion note' });
|
|
1532
|
+
f.harness.command('orchestrator', 'checkpoint');
|
|
1533
|
+
const complete = f.harness.command('orchestrator', 'goal-complete');
|
|
1534
|
+
assert.equal(complete.complete, true);
|
|
1535
|
+
assert.equal(complete.nextStep.phase, 'complete');
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1538
|
+
test('paused project can reregister with updated records', (t) => {
|
|
1539
|
+
const f = useFixture(t);
|
|
1540
|
+
activate(f);
|
|
1541
|
+
const before = f.harness.transaction((state) => state.projects[f.project].records.specs);
|
|
1542
|
+
const started = f.harness.command('orchestrator', 'reregister', { reason: 'Requirements changed after confirmation.' });
|
|
1543
|
+
assert.equal(started.reregistering.reason, 'Requirements changed after confirmation.');
|
|
1544
|
+
assert.equal(started.nextStep.phase, 'reregister-prepare');
|
|
1545
|
+
const requirements = read(f.root, `${f.project}/Documents/REQUIREMENTS.md`) + '\nUpdated requirement detail.\n';
|
|
1546
|
+
f.harness.bootstrapDocument('orchestrator', { project: f.project, path: 'Documents/REQUIREMENTS.md', content: requirements });
|
|
1547
|
+
assert.equal(f.harness.command('orchestrator', 'status').nextStep.phase, 'reregister-commit');
|
|
1548
|
+
assert.match(f.harness.bootstrapCommit('orchestrator', { project: f.project, message: 'docs: update requirements' }).next, /prepare/);
|
|
1549
|
+
const prepared = f.harness.prepare('orchestrator', { project: f.project, goal: f.goal.slice(f.project.length + 1), scene: f.sceneName });
|
|
1550
|
+
f.harness.question('orchestrator', registrationQuestion(prepared), [['Confirm registration']]);
|
|
1551
|
+
const after = f.harness.command('orchestrator', 'status');
|
|
1552
|
+
assert.equal(after.goalActive, false);
|
|
1553
|
+
assert.equal(after.nextStep.phase, 'goal');
|
|
1554
|
+
assert.equal(f.harness.transaction((state) => state.projects[f.project].reregistering), false);
|
|
1555
|
+
assert.notDeepEqual(f.harness.transaction((state) => state.projects[f.project].records.specs), before);
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
test('registered changes cannot rewrite registration documents', (t) => {
|
|
1559
|
+
const f = useFixture(t);
|
|
1560
|
+
const goal = f.goal.slice(f.project.length + 1);
|
|
1561
|
+
activate(f);
|
|
1562
|
+
begin(f, 'orchestrator', { paths: ['Documents/REQUIREMENTS.md', 'Documents/WORKFLOW.md', 'Documents/REVIEW.md', goal, 'Documents/DECISIONS.md'] });
|
|
1563
|
+
const requirements = read(f.root, `${f.project}/Documents/REQUIREMENTS.md`);
|
|
1564
|
+
const workflow = read(f.root, `${f.project}/Documents/WORKFLOW.md`);
|
|
1565
|
+
const review = read(f.root, `${f.project}/Documents/REVIEW.md`);
|
|
1566
|
+
const goalText = read(f.root, f.goal);
|
|
1567
|
+
for (const [name, content] of [['Documents/REQUIREMENTS.md', requirements], ['Documents/WORKFLOW.md', workflow], ['Documents/REVIEW.md', review], [goal, goalText]]) {
|
|
1568
|
+
gate(() => f.harness.command('orchestrator', 'document', { path: name, content }), /frozen/);
|
|
1569
|
+
}
|
|
1570
|
+
assert.equal(read(f.root, `${f.project}/Documents/REQUIREMENTS.md`), requirements);
|
|
1571
|
+
assert.equal(read(f.root, `${f.project}/Documents/REVIEW.md`), review);
|
|
1572
|
+
assert.equal(read(f.root, f.goal), goalText);
|
|
1573
|
+
assert.doesNotThrow(() => f.harness.command('orchestrator', 'document', {
|
|
1574
|
+
path: 'Documents/DECISIONS.md', content: read(f.root, `${f.project}/Documents/DECISIONS.md`) + 'Registered change.\n',
|
|
1575
|
+
}));
|
|
1576
|
+
});
|
|
1577
|
+
|
|
1578
|
+
test('partial review waits for approval before committing the reviewed change', (t) => {
|
|
1579
|
+
const f = useFixture(t, { workflow: 'Partially Autonomous', review: 'All' });
|
|
1580
|
+
activate(f);
|
|
1581
|
+
begin(f);
|
|
1582
|
+
reviewAll(f);
|
|
1583
|
+
assert.ok(Number(git(f.root, ['rev-list', '--count', 'HEAD']).trim()) >= 2);
|
|
1584
|
+
assert.equal(f.harness.command('orchestrator', 'status').nextStep.phase, 'approval');
|
|
1585
|
+
const approval = f.harness.command('orchestrator', 'approval');
|
|
1586
|
+
f.harness.question('orchestrator', {
|
|
1587
|
+
questions: [{ question: approval.question, options: approval.labels.map((label) => ({ label })), custom: false, multiple: false }],
|
|
1588
|
+
}, [['Approve change']]);
|
|
1589
|
+
assert.equal(f.harness.command('orchestrator', 'status').nextStep.phase, 'commit');
|
|
1590
|
+
f.harness.command('orchestrator', 'commit', { message: 'feat: approved fixture change' });
|
|
1591
|
+
f.harness.command('orchestrator', 'checkpoint');
|
|
1592
|
+
assert.ok(Number(git(f.root, ['rev-list', '--count', 'HEAD']).trim()) >= 3);
|
|
1593
|
+
});
|
|
1594
|
+
|
|
1595
|
+
test('review auto-commits reports and the verdict before checkpoint', (t) => {
|
|
1596
|
+
const f = useFixture(t, { review: 'All' });
|
|
1597
|
+
activate(f);
|
|
1598
|
+
begin(f);
|
|
1599
|
+
reviewAll(f);
|
|
1600
|
+
assert.equal(git(f.root, ['status', '--porcelain']).trim(), '');
|
|
1601
|
+
assert.ok(Number(git(f.root, ['rev-list', '--count', 'HEAD']).trim()) >= 3);
|
|
1602
|
+
const review = read(f.root, `${f.project}/Documents/Reviews/R01-20261031T110000Z/REVIEW.md`);
|
|
1603
|
+
assert.match(review, /Verdict: PASS/);
|
|
1604
|
+
assert.match(review, /## Reason/);
|
|
1605
|
+
const status = f.harness.command('orchestrator', 'checkpoint');
|
|
1606
|
+
assert.equal(status.change, null);
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
test('review writes the round index before auto-commit', (t) => {
|
|
1610
|
+
const f = useFixture(t, { review: 'All' });
|
|
1611
|
+
activate(f);
|
|
1612
|
+
begin(f);
|
|
1613
|
+
const reviewed = reviewAll(f);
|
|
1614
|
+
const index = read(f.root, `${f.project}/Documents/Reviews/README.md`);
|
|
1615
|
+
assert.match(index, new RegExp(`\\| ${reviewed.round} \\| 20261031T110000Z \\| [a-f0-9]{12} \\| All final \\| visual \\| PASS \\|`));
|
|
1616
|
+
assert.match(index, new RegExp(`\\[${reviewed.round}/REVIEW\\.md\\]`));
|
|
1617
|
+
assert.doesNotMatch(index, /\| — \| — \|/);
|
|
1618
|
+
assert.equal(git(f.root, ['status', '--porcelain']).trim(), '');
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
test('commit helper stages the exact scoped change', (t) => {
|
|
1622
|
+
const f = useFixture(t);
|
|
1623
|
+
activate(f);
|
|
1624
|
+
begin(f);
|
|
1625
|
+
seal(f);
|
|
1626
|
+
selfReview(f);
|
|
1627
|
+
f.harness.command('orchestrator', 'commit', { message: 'feat: bounded fixture change' });
|
|
1628
|
+
assert.equal(git(f.root, ['status', '--porcelain']).trim(), '');
|
|
1629
|
+
const status = f.harness.command('orchestrator', 'checkpoint');
|
|
1630
|
+
assert.equal(status.change, null);
|
|
1631
|
+
assert.equal(f.harness.transaction((s) => s.projects[f.project].history.at(-1).verdict), 'PASS');
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
test('checkpoint accepts a scoped commit range and completes goal after PASS', (t) => {
|
|
1635
|
+
const f = useFixture(t);
|
|
1636
|
+
activate(f);
|
|
1637
|
+
begin(f);
|
|
1638
|
+
seal(f);
|
|
1639
|
+
selfReview(f);
|
|
1640
|
+
gate(() => begin(f));
|
|
1641
|
+
gate(() => f.harness.command('orchestrator', 'goal-complete'));
|
|
1642
|
+
gate(() => f.harness.command('orchestrator', 'checkpoint'));
|
|
1643
|
+
write(f.root, `${f.project}/notes.txt`, 'approved change\n');
|
|
1644
|
+
scopedCommit(f);
|
|
1645
|
+
f.harness.command('orchestrator', 'checkpoint');
|
|
1646
|
+
assert.ok(Number(git(f.root, ['rev-list', '--count', 'HEAD']).trim()) >= 3);
|
|
1647
|
+
const complete = f.harness.command('orchestrator', 'goal-complete');
|
|
1648
|
+
assert.equal(complete.complete, true);
|
|
1649
|
+
assert.equal(complete.change, null);
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
test('a paused or cleared goal keeps an open change resumable', (t) => {
|
|
1653
|
+
const f = useFixture(t);
|
|
1654
|
+
activate(f);
|
|
1655
|
+
begin(f);
|
|
1656
|
+
const paused = f.harness.command('orchestrator', 'goal-inactive');
|
|
1657
|
+
assert.equal(paused.goalActive, false);
|
|
1658
|
+
assert.equal(paused.nextStep.phase, 'resume-goal');
|
|
1659
|
+
assert.match(paused.nextStep.command, /goal \{action:"resume"\}/);
|
|
1660
|
+
f.harness.command('orchestrator', 'goal-active', {});
|
|
1661
|
+
assert.equal(f.harness.command('orchestrator', 'status').nextStep.phase, 'apply');
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
test('goal completion requires active goal after a clean checkpoint', (t) => {
|
|
1665
|
+
const f = useFixture(t);
|
|
1666
|
+
activate(f);
|
|
1667
|
+
begin(f);
|
|
1668
|
+
seal(f);
|
|
1669
|
+
selfReview(f);
|
|
1670
|
+
write(f.root, `${f.project}/notes.txt`, 'approved change\n');
|
|
1671
|
+
scopedCommit(f);
|
|
1672
|
+
f.harness.command('orchestrator', 'checkpoint');
|
|
1673
|
+
f.harness.command('orchestrator', 'goal-complete');
|
|
1674
|
+
assert.equal(f.harness.command('orchestrator', 'status').complete, true);
|
|
1675
|
+
assert.match(read(f.root, `${f.project}/Documents/TASKSTRACKER.md`), /^Status: Complete$/m);
|
|
1676
|
+
assert.equal(git(f.root, ['status', '--porcelain']).trim(), '');
|
|
1677
|
+
});
|
|
1678
|
+
|
|
1679
|
+
test('strict milestone commits each apply, requires finalize/WIP look, and accepts a commit range', (t) => {
|
|
1680
|
+
const f = useFixture(t);
|
|
1681
|
+
activate(f);
|
|
1682
|
+
begin(f, 'orchestrator', { milestone: 'Fixture milestone', requirements: ['R1'], final: true });
|
|
1683
|
+
for (let index = 0; index < 2; index += 1) {
|
|
1684
|
+
const issued = issueAndConsume(f, 'orchestrator', 'apply', { index });
|
|
1685
|
+
if (index === 0) f.harness.bindWorker('orchestrator', 'child-worker', `Ticket: ${issued.token}`);
|
|
1686
|
+
f.harness.finish(issued.token, { ok: true, completed: 1, operationResults: [{ index: 1, operation: 'create_mesh', ok: true }] });
|
|
1687
|
+
}
|
|
1688
|
+
let status = f.harness.command('orchestrator', 'status');
|
|
1689
|
+
assert.equal(status.change.commits.length, 2);
|
|
1690
|
+
assert.equal(status.nextStep.phase, 'apply');
|
|
1691
|
+
assert.equal(git(f.root, ['status', '--porcelain', '--', f.project]).trim(), '');
|
|
1692
|
+
const tracker = read(f.root, `${f.project}/Documents/TASKSTRACKER.md`);
|
|
1693
|
+
assert.match(tracker, /^# Task Tracker — Test/);
|
|
1694
|
+
assert.match(tracker, /Tasks\/T001\/TASK\.md/);
|
|
1695
|
+
assert.match(read(f.root, `${f.project}/Documents/Tasks/T001/TASK.md`), /Child session: child-worker/);
|
|
1696
|
+
assert.ok(fs.existsSync(path.join(f.root, f.project, 'Documents', 'Tasks', 'T001', 'SUMMARY.md')));
|
|
1697
|
+
f.harness.command('orchestrator', 'finalize');
|
|
1698
|
+
status = f.harness.command('orchestrator', 'status');
|
|
1699
|
+
assert.equal(status.nextStep.phase, 'wip');
|
|
1700
|
+
const wipPath = `${f.project}/renders/wip/WIP-test.png`;
|
|
1701
|
+
write(f.root, wipPath, Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
|
|
1702
|
+
const wip = issueAndConsume(f, 'orchestrator', 'wip');
|
|
1703
|
+
f.harness.finish(wip.token, { ok: true, renderPath: wipPath });
|
|
1704
|
+
assert.equal(f.harness.command('orchestrator', 'status').nextStep.phase, 'wip-inspect');
|
|
1705
|
+
f.harness.inspect('orchestrator', path.resolve(f.root, wipPath), true);
|
|
1706
|
+
assert.equal(f.harness.command('orchestrator', 'status').nextStep.phase, 'seal');
|
|
1707
|
+
seal(f);
|
|
1708
|
+
selfReview(f);
|
|
1709
|
+
f.harness.command('orchestrator', 'commit', { message: 'feat: strict fixture change' });
|
|
1710
|
+
const closed = f.harness.command('orchestrator', 'checkpoint');
|
|
1711
|
+
assert.equal(closed.change, null);
|
|
1712
|
+
assert.ok(f.harness.transaction((s) => s.projects[f.project].history.at(-1).final));
|
|
1713
|
+
assert.equal(f.harness.command('orchestrator', 'goal-complete').complete, true);
|
|
1714
|
+
});
|
|
1715
|
+
|
|
1716
|
+
test('every third apply requires a WIP render before continuing', (t) => {
|
|
1717
|
+
const f = useFixture(t);
|
|
1718
|
+
activate(f);
|
|
1719
|
+
begin(f);
|
|
1720
|
+
for (let index = 0; index < 3; index += 1) {
|
|
1721
|
+
const issued = issueAndConsume(f, 'orchestrator', 'apply', { index });
|
|
1722
|
+
f.harness.finish(issued.token, { ok: true });
|
|
1723
|
+
}
|
|
1724
|
+
assert.equal(f.harness.command('orchestrator', 'status').nextStep.phase, 'wip');
|
|
1725
|
+
gate(() => f.harness.issue('orchestrator', 'apply', { index: 4 }), /WIP.*third apply|third apply.*WIP/i);
|
|
1726
|
+
const wipPath = `${f.project}/renders/wip/WIP-third.png`;
|
|
1727
|
+
write(f.root, wipPath, Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
|
|
1728
|
+
const wip = issueAndConsume(f, 'orchestrator', 'wip');
|
|
1729
|
+
f.harness.finish(wip.token, { ok: true, renderPath: wipPath });
|
|
1730
|
+
assert.equal(f.harness.command('orchestrator', 'status').nextStep.phase, 'wip-inspect');
|
|
1731
|
+
gate(() => f.harness.issue('orchestrator', 'apply', { index: 4 }), /WIP.*third apply|third apply.*WIP/i);
|
|
1732
|
+
f.harness.inspect('orchestrator', path.resolve(f.root, wipPath), true);
|
|
1733
|
+
assert.equal(f.harness.command('orchestrator', 'status').nextStep.phase, 'apply');
|
|
1734
|
+
});
|
|
1735
|
+
|
|
1736
|
+
test('a failed worker task commits its task metadata and leaves the project clean', (t) => {
|
|
1737
|
+
const f = useFixture(t);
|
|
1738
|
+
activate(f);
|
|
1739
|
+
begin(f);
|
|
1740
|
+
const issued = f.harness.issue('orchestrator', 'apply', { index: 1 });
|
|
1741
|
+
f.harness.failWorkerByToken('orchestrator', issued.token, 'worker crashed before apply');
|
|
1742
|
+
const task = f.harness.transaction((s) => s.projects[f.project].tasks.find((item) => item.id === issued.taskId));
|
|
1743
|
+
assert.equal(task.state, 'failed');
|
|
1744
|
+
assert.match(task.result, /worker crashed/);
|
|
1745
|
+
assert.ok(fs.existsSync(path.join(f.root, task.summaryDoc)));
|
|
1746
|
+
assert.equal(git(f.root, ['status', '--porcelain', '--', f.project]).trim(), '');
|
|
1747
|
+
assert.equal(f.harness.command('orchestrator', 'status').change.commits.length, 1);
|
|
1748
|
+
assert.notEqual(f.harness.command('orchestrator', 'status').change, null);
|
|
1749
|
+
});
|
|
1750
|
+
|
|
1751
|
+
test('a failed apply commits task metadata without committing the failed scene', (t) => {
|
|
1752
|
+
const f = useFixture(t);
|
|
1753
|
+
activate(f);
|
|
1754
|
+
begin(f);
|
|
1755
|
+
const issued = issueAndConsume(f, 'orchestrator', 'apply', { fail: true });
|
|
1756
|
+
f.harness.bindWorkerByToken('orchestrator', 'child-worker', issued.token);
|
|
1757
|
+
f.harness.finish(issued.token, { ok: false, error: 'controlled apply failure' });
|
|
1758
|
+
const task = f.harness.transaction((s) => s.projects[f.project].tasks.find((item) => item.id === issued.taskId));
|
|
1759
|
+
assert.equal(task.state, 'failed');
|
|
1760
|
+
assert.ok(task.child);
|
|
1761
|
+
assert.equal(git(f.root, ['status', '--porcelain', '--', f.project]).trim(), '');
|
|
1762
|
+
assert.equal(f.harness.command('orchestrator', 'status').change.commits.length, 1);
|
|
1763
|
+
});
|
|
1764
|
+
|
|
1765
|
+
test('checkpoint rejects unrelated files and accepts scoped commit ranges', (t) => {
|
|
1766
|
+
const unrelated = useFixture(t);
|
|
1767
|
+
activate(unrelated);
|
|
1768
|
+
begin(unrelated);
|
|
1769
|
+
seal(unrelated);
|
|
1770
|
+
selfReview(unrelated);
|
|
1771
|
+
write(unrelated.root, `${unrelated.project}/notes.txt`, 'approved change\n');
|
|
1772
|
+
write(unrelated.root, `${unrelated.project}/unrelated.txt`, 'outside scope\n');
|
|
1773
|
+
commit(unrelated.root, 'unrelated change', [`${unrelated.project}/notes.txt`, `${unrelated.project}/unrelated.txt`]);
|
|
1774
|
+
gate(() => unrelated.harness.command('orchestrator', 'checkpoint'));
|
|
1775
|
+
|
|
1776
|
+
const multiple = useFixture(t);
|
|
1777
|
+
activate(multiple);
|
|
1778
|
+
begin(multiple);
|
|
1779
|
+
seal(multiple);
|
|
1780
|
+
selfReview(multiple);
|
|
1781
|
+
write(multiple.root, `${multiple.project}/notes.txt`, 'first commit\n');
|
|
1782
|
+
scopedCommit(multiple, 'first change');
|
|
1783
|
+
write(multiple.root, `${multiple.project}/notes.txt`, 'second commit\n');
|
|
1784
|
+
scopedCommit(multiple, 'second change');
|
|
1785
|
+
assert.equal(multiple.harness.command('orchestrator', 'checkpoint').change, null);
|
|
1786
|
+
});
|
|
1787
|
+
|
|
1788
|
+
test('state survives Harness restart and state lock blocks writers', (t) => {
|
|
1789
|
+
const f = useFixture(t);
|
|
1790
|
+
activate(f);
|
|
1791
|
+
const restarted = new Harness(f.root, { stateRoot: f.stateRoot });
|
|
1792
|
+
assert.equal(path.dirname(restarted.home), tempRoot);
|
|
1793
|
+
assert.equal(restarted.home.startsWith(f.root + path.sep), false);
|
|
1794
|
+
assert.equal(restarted.command('orchestrator', 'status').goalActive, true);
|
|
1795
|
+
f.harness.transaction(() => gate(() => restarted.command('orchestrator', 'status')));
|
|
1796
|
+
const started = begin(f);
|
|
1797
|
+
const again = new Harness(f.root, { stateRoot: f.stateRoot });
|
|
1798
|
+
const stableStatus = (status) => {
|
|
1799
|
+
const copy = structuredClone(status);
|
|
1800
|
+
if (copy.change) delete copy.change.deadlineRemainingMs;
|
|
1801
|
+
return copy;
|
|
1802
|
+
};
|
|
1803
|
+
assert.deepEqual(stableStatus(again.command('orchestrator', 'status')), stableStatus(started));
|
|
1804
|
+
});
|
|
1805
|
+
|
|
1806
|
+
test('pause is explicit, requires a closed change, and stops the active goal', (t) => {
|
|
1807
|
+
const f = useFixture(t);
|
|
1808
|
+
activate(f);
|
|
1809
|
+
begin(f);
|
|
1810
|
+
gate(() => f.harness.command('orchestrator', 'pause', { reason: 'User requested stop.' }), /close the open change/);
|
|
1811
|
+
f.harness.transaction((s) => { s.projects[f.project].change = null; });
|
|
1812
|
+
const paused = f.harness.command('orchestrator', 'pause', { reason: 'User requested stop.' });
|
|
1813
|
+
assert.equal(paused.goalActive, false);
|
|
1814
|
+
assert.equal(paused.nextStep.phase, 'goal');
|
|
1815
|
+
gate(() => begin(f));
|
|
1816
|
+
});
|
|
1817
|
+
|
|
1818
|
+
test('reclaim requires a stale window and transfers ownership explicitly', (t) => {
|
|
1819
|
+
const f = useFixture(t);
|
|
1820
|
+
activate(f);
|
|
1821
|
+
begin(f);
|
|
1822
|
+
f.harness.transaction((s) => {
|
|
1823
|
+
s.projects[f.project].orchestrator = 'old-session';
|
|
1824
|
+
s.projects[f.project].change.deadline = Date.now() + 60_000;
|
|
1825
|
+
});
|
|
1826
|
+
gate(() => f.harness.command('new-session', 'reclaim', { project: f.project, reason: 'test' }), /live change window/);
|
|
1827
|
+
f.harness.transaction((s) => { s.projects[f.project].change.deadline = Date.now() - 1; });
|
|
1828
|
+
const reclaimed = f.harness.command('new-session', 'reclaim', { project: f.project, reason: 'old session stopped responding' });
|
|
1829
|
+
assert.equal(reclaimed.goalActive, true);
|
|
1830
|
+
assert.equal(f.harness.command('new-session', 'status').project, f.project);
|
|
1831
|
+
});
|
|
1832
|
+
|
|
1833
|
+
test('registration rejects takeover of project owned by another active orchestrator', (t) => {
|
|
1834
|
+
const f = useFixture(t);
|
|
1835
|
+
activate(f);
|
|
1836
|
+
gate(() => f.harness.prepare('second-orchestrator', {
|
|
1837
|
+
project: f.project,
|
|
1838
|
+
goal: f.goal.slice(f.project.length + 1),
|
|
1839
|
+
scene: f.sceneName,
|
|
1840
|
+
}));
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
test('registered projects open independent logical changes', (t) => {
|
|
1844
|
+
const f = useFixture(t);
|
|
1845
|
+
const other = { ...f, ...createProject(f.root, { name: 'Other' }) };
|
|
1846
|
+
commit(f.root, 'second scaffold', [other.project]);
|
|
1847
|
+
activate(f);
|
|
1848
|
+
activate(other, 'other-orchestrator');
|
|
1849
|
+
const first = begin(f);
|
|
1850
|
+
const second = begin(other, 'other-orchestrator');
|
|
1851
|
+
assert.notEqual(first.change.id, second.change.id);
|
|
1852
|
+
assert.equal(first.scene, f.scene);
|
|
1853
|
+
assert.deepEqual(first.change.paths, [`${f.project}/notes.txt`]);
|
|
1854
|
+
assert.equal(f.harness.command('orchestrator', 'status').change.id, first.change.id);
|
|
1855
|
+
assert.equal(f.harness.command('other-orchestrator', 'status').change.id, second.change.id);
|
|
1856
|
+
gate(() => begin(f));
|
|
1857
|
+
});
|
|
1858
|
+
|
|
1859
|
+
test('external binary policy requires explicit redistribution clearance', (t) => {
|
|
1860
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'kilo-binary-policy-'));
|
|
1861
|
+
t.after(() => fs.rmSync(root, { force: true, recursive: true }));
|
|
1862
|
+
const project = 'Blender/Policy';
|
|
1863
|
+
const asset = `${project}/assets/external/vendor/model.glb`;
|
|
1864
|
+
const source = `${project}/assets/external/vendor/SOURCE.md`;
|
|
1865
|
+
write(root, asset, GLB);
|
|
1866
|
+
write(root, source, '- Redistribution allowed: no\n');
|
|
1867
|
+
assert.throws(() => externalBinaryPolicy(root, project, [asset]), /not cleared for commit/);
|
|
1868
|
+
write(root, source, '- Redistribution allowed: yes\n');
|
|
1869
|
+
assert.doesNotThrow(() => externalBinaryPolicy(root, project, [asset]));
|
|
1870
|
+
assert.doesNotThrow(() => externalBinaryPolicy(root, project, [source]));
|
|
1871
|
+
});
|
|
1872
|
+
|
|
1873
|
+
test('contained and project methods reject traversal and symlink paths', (t) => {
|
|
1874
|
+
const f = useFixture(t);
|
|
1875
|
+
gate(() => contained(f.root, '../outside.txt'));
|
|
1876
|
+
activate(f);
|
|
1877
|
+
const outside = path.join(f.root, 'outside');
|
|
1878
|
+
fs.mkdirSync(outside);
|
|
1879
|
+
fs.writeFileSync(path.join(outside, 'file.txt'), 'outside\n');
|
|
1880
|
+
const linked = path.join(f.root, f.project, 'linked');
|
|
1881
|
+
try {
|
|
1882
|
+
fs.symlinkSync(outside, linked, process.platform === 'win32' ? 'junction' : 'dir');
|
|
1883
|
+
} catch (error) {
|
|
1884
|
+
t.skip(`symlink creation unavailable: ${error.code ?? error.message}`);
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
gate(() => f.harness.command('orchestrator', 'begin', {
|
|
1888
|
+
outcome: 'linked path',
|
|
1889
|
+
objects: ['FixtureCube'],
|
|
1890
|
+
paths: ['linked/file.txt'],
|
|
1891
|
+
}));
|
|
1892
|
+
fs.unlinkSync(linked);
|
|
1893
|
+
const linkedState = path.join(tempRoot, `state-link-${process.pid}-${Date.now()}`);
|
|
1894
|
+
try {
|
|
1895
|
+
fs.symlinkSync(f.stateRoot, linkedState, process.platform === 'win32' ? 'junction' : 'dir');
|
|
1896
|
+
gate(() => new Harness(f.root, { stateRoot: linkedState }));
|
|
1897
|
+
} finally {
|
|
1898
|
+
if (fs.existsSync(linkedState)) fs.unlinkSync(linkedState);
|
|
1899
|
+
}
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
test('a successful WIP task commits its metadata and leaves the project clean', (t) => {
|
|
1903
|
+
const f = useFixture(t);
|
|
1904
|
+
activate(f);
|
|
1905
|
+
begin(f);
|
|
1906
|
+
const applied = issueAndConsume(f, 'orchestrator', 'apply', { index: 1 });
|
|
1907
|
+
f.harness.finish(applied.token, { ok: true });
|
|
1908
|
+
f.harness.command('orchestrator', 'finalize');
|
|
1909
|
+
const wipPath = f.project + '/renders/wip/WIP-task-commit.png';
|
|
1910
|
+
write(f.root, wipPath, Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
|
|
1911
|
+
const wip = issueAndConsume(f, 'orchestrator', 'wip');
|
|
1912
|
+
f.harness.finish(wip.token, { ok: true, renderPath: wipPath });
|
|
1913
|
+
const task = f.harness.transaction((s) => s.projects[f.project].tasks.find((item) => item.id === wip.taskId));
|
|
1914
|
+
assert.equal(task.state, 'done');
|
|
1915
|
+
assert.ok(fs.existsSync(path.join(f.root, task.summaryDoc)));
|
|
1916
|
+
assert.equal(git(f.root, ['status', '--porcelain', '--', f.project]).trim(), '');
|
|
1917
|
+
assert.equal(f.harness.command('orchestrator', 'status').change.commits.length, 2);
|
|
1918
|
+
});
|
|
1919
|
+
|
|
1920
|
+
test('seal and grader task metadata commit at the task boundary', (t) => {
|
|
1921
|
+
const f = useFixture(t, { review: 'All' });
|
|
1922
|
+
activate(f);
|
|
1923
|
+
begin(f);
|
|
1924
|
+
seal(f);
|
|
1925
|
+
const sealCommit = git(f.root, ['log', '-1', '--format=%s']).trim();
|
|
1926
|
+
assert.match(sealCommit, /^task: T\d+ done$/);
|
|
1927
|
+
assert.equal(git(f.root, ['status', '--porcelain', '--', `${f.project}/Documents/TASKSTRACKER.md`, `${f.project}/Documents/Tasks`]).trim(), '');
|
|
1928
|
+
|
|
1929
|
+
const ticket = f.harness.command('orchestrator', 'grader-ticket', { name: 'visual' });
|
|
1930
|
+
f.harness.bindGrader(
|
|
1931
|
+
'orchestrator',
|
|
1932
|
+
'child-grader',
|
|
1933
|
+
`Review assignment ${ticket.marker}`,
|
|
1934
|
+
{ providerID: 'fixture-provider', modelID: 'fixture-model', variant: 'fixture-variant' },
|
|
1935
|
+
);
|
|
1936
|
+
write(f.root, ticket.report, [
|
|
1937
|
+
'# Visual report',
|
|
1938
|
+
'- Verdict: PASS',
|
|
1939
|
+
`Scene hash: ${ticket.sceneHash}`,
|
|
1940
|
+
`Evidence key: ${ticket.evidenceKey}`,
|
|
1941
|
+
'',
|
|
1942
|
+
].join('\n'));
|
|
1943
|
+
f.harness.collectGrader('orchestrator', 'child-grader');
|
|
1944
|
+
assert.match(git(f.root, ['log', '-1', '--format=%s']).trim(), /^task: G\d+ done$/);
|
|
1945
|
+
assert.equal(git(f.root, ['status', '--porcelain', '--', `${f.project}/Documents/TASKSTRACKER.md`, `${f.project}/Documents/Tasks`]).trim(), '');
|
|
1946
|
+
});
|
|
1947
|
+
|
|
1948
|
+
test('roster drift permits goal sync but blocks new work', (t) => {
|
|
1949
|
+
const f = useFixture(t, { review: 'All' });
|
|
1950
|
+
activate(f);
|
|
1951
|
+
const file = path.join(f.root, '.kilo', 'blender-project-harness', 'config.json');
|
|
1952
|
+
const config = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1953
|
+
config.blender_subagents.graders.graders[0].backup.model = 'changed-backup';
|
|
1954
|
+
fs.writeFileSync(file, JSON.stringify(config));
|
|
1955
|
+
assert.doesNotThrow(() => f.harness.command('orchestrator', 'goal-active'));
|
|
1956
|
+
gate(() => begin(f), /roster changed/);
|
|
1957
|
+
});
|
|
1958
|
+
|
|
1959
|
+
test('task metadata records the governing ticket deadline', (t) => {
|
|
1960
|
+
const f = useFixture(t, { review: 'All' });
|
|
1961
|
+
activate(f);
|
|
1962
|
+
begin(f);
|
|
1963
|
+
const changeDeadline = f.harness.transaction((s) => s.projects[f.project].change.deadline);
|
|
1964
|
+
const apply = issueAndConsume(f, 'orchestrator', 'apply', { deadlineCheck: true });
|
|
1965
|
+
f.harness.finish(apply.token, { ok: true });
|
|
1966
|
+
const applyTask = f.harness.transaction((s) => s.projects[f.project].tasks.find((task) => task.token === apply.token));
|
|
1967
|
+
assert.ok(applyTask.deadline < changeDeadline);
|
|
1968
|
+
|
|
1969
|
+
seal(f);
|
|
1970
|
+
const ticket = f.harness.command('orchestrator', 'grader-ticket', { name: 'visual' });
|
|
1971
|
+
const grader = f.harness.transaction((s) => s.projects[f.project].change.graders.visual);
|
|
1972
|
+
const graderTask = f.harness.transaction((s) => s.projects[f.project].tasks.find((task) => task.token === ticket.token));
|
|
1973
|
+
assert.equal(graderTask.deadline, grader.deadline);
|
|
1974
|
+
});
|
|
1975
|
+
|
|
1976
|
+
test('synthetic live run: three-grader review, revision, and blocked recovery', (t) => {
|
|
1977
|
+
const f = useFixture(t, { review: 'All', configValue: threeGraderConfig() });
|
|
1978
|
+
activate(f);
|
|
1979
|
+
begin(f, 'orchestrator', {
|
|
1980
|
+
outcome: 'Build the cozy room shell',
|
|
1981
|
+
objects: ['Room_Floor', 'Room_Wall_Back', 'Window'],
|
|
1982
|
+
paths: ['notes.txt'],
|
|
1983
|
+
final: false,
|
|
1984
|
+
milestone: 'Room shell',
|
|
1985
|
+
requirements: ['REQ-01'],
|
|
1986
|
+
});
|
|
1987
|
+
for (let index = 0; index < 3; index += 1) simulationApply(f, index);
|
|
1988
|
+
seal(f, 'orchestrator', { round: 'R01-20261102T100000Z' });
|
|
1989
|
+
const visual = simulationGrader(f, 'visual', 'CHANGES_REQUIRED', { backup: true });
|
|
1990
|
+
assert.equal(visual.result.change.graders.visual.modelRole, 'backup');
|
|
1991
|
+
simulationGrader(f, 'technical', 'PASS');
|
|
1992
|
+
simulationGrader(f, 'spec', 'CHANGES_REQUIRED');
|
|
1993
|
+
const firstReview = simulationReview(f, 'The shell milestone is reviewed.');
|
|
1994
|
+
assert.equal(firstReview.change.verdict, 'CHANGES_REQUIRED');
|
|
1995
|
+
f.harness.command('orchestrator', 'checkpoint');
|
|
1996
|
+
|
|
1997
|
+
begin(f, 'orchestrator', {
|
|
1998
|
+
outcome: 'Complete the cozy room',
|
|
1999
|
+
objects: ['Desk', 'Monitor', 'Chair'],
|
|
2000
|
+
paths: ['notes.txt'],
|
|
2001
|
+
final: true,
|
|
2002
|
+
revision: true,
|
|
2003
|
+
milestone: 'Final room',
|
|
2004
|
+
requirements: ['REQ-02'],
|
|
2005
|
+
});
|
|
2006
|
+
for (let index = 0; index < 2; index += 1) simulationApply(f, index + 3);
|
|
2007
|
+
seal(f, 'orchestrator', { round: 'R02-20261102T110000Z' });
|
|
2008
|
+
simulationGrader(f, 'visual', 'CHANGES_REQUIRED');
|
|
2009
|
+
simulationGrader(f, 'technical', 'PASS');
|
|
2010
|
+
simulationGrader(f, 'spec', 'BLOCKED');
|
|
2011
|
+
const retry = f.harness.command('orchestrator', 'grader-ticket', { name: 'spec' });
|
|
2012
|
+
assert.ok(retry.token);
|
|
2013
|
+
assert.equal(f.harness.transaction((s) => s.projects[f.project].change.graders.spec.attempt), 2);
|
|
2014
|
+
f.harness.command('orchestrator', 'grader-fail', { name: 'spec', reason: 'the retry never started a child session' });
|
|
2015
|
+
assert.equal(f.harness.command('orchestrator', 'status').nextStep.phase, 'review');
|
|
2016
|
+
const blocked = simulationReview(f, 'The R02 spec grader could not report.');
|
|
2017
|
+
assert.equal(blocked.change.verdict, 'BLOCKED');
|
|
2018
|
+
assert.equal(blocked.change.blockingKind, 'tooling');
|
|
2019
|
+
assert.equal(f.harness.command('orchestrator', 'status').nextStep.phase, 'preserve');
|
|
2020
|
+
const preserved = f.harness.command('orchestrator', 'preserve', { reason: 'Preserve the sealed R02 evidence for a later grader retry.' });
|
|
2021
|
+
assert.equal(preserved.change.verdict, 'WIP');
|
|
2022
|
+
f.harness.command('orchestrator', 'checkpoint');
|
|
2023
|
+
const paused = f.harness.command('orchestrator', 'pause', { reason: 'Synthetic run stopped after preserving a tooling block.' });
|
|
2024
|
+
assert.equal(paused.goalActive, false);
|
|
2025
|
+
assert.equal(git(f.root, ['status', '--porcelain', '--', f.project]).trim(), '');
|
|
2026
|
+
assert.equal(f.harness.transaction((s) => s.projects[f.project].history.at(-1).verdict), 'WIP');
|
|
2027
|
+
});
|
|
2028
|
+
|
|
2029
|
+
test('synthetic live run: final all-grader PASS completes the goal', (t) => {
|
|
2030
|
+
const f = useFixture(t, { review: 'All', configValue: threeGraderConfig() });
|
|
2031
|
+
activate(f);
|
|
2032
|
+
begin(f, 'orchestrator', {
|
|
2033
|
+
outcome: 'Deliver the final cozy room',
|
|
2034
|
+
objects: ['Room_Floor', 'Desk', 'Monitor'],
|
|
2035
|
+
paths: ['notes.txt'],
|
|
2036
|
+
final: true,
|
|
2037
|
+
milestone: 'Final room',
|
|
2038
|
+
requirements: ['REQ-01', 'REQ-02'],
|
|
2039
|
+
});
|
|
2040
|
+
simulationApply(f, 0);
|
|
2041
|
+
simulationAssetCheck(f);
|
|
2042
|
+
const sealed = seal(f, 'orchestrator', { round: 'R01-20261102T120000Z' });
|
|
2043
|
+
const manifest = JSON.parse(read(f.root, sealed.manifestPath));
|
|
2044
|
+
assert.equal(manifest.asset_check.strict, true);
|
|
2045
|
+
assert.deepEqual(manifest.asset_check.packed, ['fixture.png']);
|
|
2046
|
+
const facts = JSON.parse(read(f.root, sealed.factsPath));
|
|
2047
|
+
assert.deepEqual(facts.objects[0].dimensions, [2, 2, 2]);
|
|
2048
|
+
assert.equal(facts.objects[0].mesh.non_manifold_edges, 0);
|
|
2049
|
+
const visual = simulationGrader(f, 'visual', 'PASS');
|
|
2050
|
+
assert.match(visual.ticket.prompt, new RegExp(`saved scene SHA-256 ${visual.ticket.sceneHash}`));
|
|
2051
|
+
assert.match(visual.ticket.prompt, new RegExp(`Evidence key: ${visual.ticket.evidenceKey}`));
|
|
2052
|
+
simulationGrader(f, 'technical', 'PASS');
|
|
2053
|
+
simulationGrader(f, 'spec', 'PASS');
|
|
2054
|
+
const reviewed = simulationReview(f, 'All three graders passed the final room.');
|
|
2055
|
+
assert.equal(reviewed.change.verdict, 'PASS');
|
|
2056
|
+
f.harness.command('orchestrator', 'checkpoint');
|
|
2057
|
+
f.harness.command('orchestrator', 'goal-complete');
|
|
2058
|
+
assert.equal(f.harness.command('orchestrator', 'status').complete, true);
|
|
2059
|
+
});
|
|
2060
|
+
|
|
2061
|
+
test('synthetic grader reads project reference documents', (t) => {
|
|
2062
|
+
const f = useFixture(t, { review: 'Single' });
|
|
2063
|
+
activate(f);
|
|
2064
|
+
begin(f);
|
|
2065
|
+
simulationApply(f, 0);
|
|
2066
|
+
seal(f);
|
|
2067
|
+
const ticket = f.harness.command('orchestrator', 'grader-ticket', { name: 'visual' });
|
|
2068
|
+
const child = 'child-reference';
|
|
2069
|
+
f.harness.bindGrader('orchestrator', child, `Review assignment ${ticket.marker}`, {
|
|
2070
|
+
providerID: 'fixture-provider', modelID: 'fixture-model', variant: 'fixture-variant',
|
|
2071
|
+
});
|
|
2072
|
+
const document = f.harness.documentRead(child, {
|
|
2073
|
+
project: f.project,
|
|
2074
|
+
path: 'Documents/References/SCENE-HEALTH.md',
|
|
2075
|
+
});
|
|
2076
|
+
assert.equal(document.path, 'Documents/References/SCENE-HEALTH.md');
|
|
2077
|
+
assert.match(document.title, /Scene Health/);
|
|
2078
|
+
write(f.root, ticket.report, simulationReport(ticket, 'PASS'));
|
|
2079
|
+
f.harness.collectGrader('orchestrator', child);
|
|
2080
|
+
const reviewed = simulationReview(f, 'The grader read the project reference evidence.');
|
|
2081
|
+
assert.equal(reviewed.change.verdict, 'PASS');
|
|
2082
|
+
f.harness.command('orchestrator', 'checkpoint');
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
test('synthetic live run: abrupt restart keeps the change and recovers an expired ticket', (t) => {
|
|
2086
|
+
const f = useFixture(t);
|
|
2087
|
+
activate(f);
|
|
2088
|
+
const started = begin(f, 'orchestrator', { outcome: 'Recover after a restarted orchestrator', paths: ['notes.txt'], final: false });
|
|
2089
|
+
const pending = f.harness.issue('orchestrator', 'apply', { resumed: true });
|
|
2090
|
+
const restarted = new Harness(f.root, { stateRoot: f.stateRoot });
|
|
2091
|
+
const before = restarted.command('orchestrator', 'status');
|
|
2092
|
+
assert.equal(before.change.id, started.change.id);
|
|
2093
|
+
assert.equal(before.change.pending, 'operation in progress');
|
|
2094
|
+
f.harness.transaction((s) => { s.tickets[pending.token].created = Date.now() - 600_001; });
|
|
2095
|
+
const recovered = restarted.recover('orchestrator');
|
|
2096
|
+
assert.equal(recovered.change.id, started.change.id);
|
|
2097
|
+
assert.equal(recovered.change.pending, null);
|
|
2098
|
+
assert.equal(restarted.command('orchestrator', 'status').change.operations, 0);
|
|
2099
|
+
});
|
|
2100
|
+
|
|
2101
|
+
test('source-only scripts are allowed only under scripts', (t) => {
|
|
2102
|
+
const allowed = useFixture(t);
|
|
2103
|
+
activate(allowed);
|
|
2104
|
+
begin(allowed, 'orchestrator', { paths: ['scripts/build.py'] });
|
|
2105
|
+
assert.equal(allowed.harness.command('orchestrator', 'status').change.documentOnly, true);
|
|
2106
|
+
write(allowed.root, `${allowed.project}/scripts/build.py`, 'print("hello")\n');
|
|
2107
|
+
allowed.harness.command('orchestrator', 'commit', { message: 'docs: add source-only script' });
|
|
2108
|
+
allowed.harness.command('orchestrator', 'checkpoint');
|
|
2109
|
+
const denied = useFixture(t);
|
|
2110
|
+
activate(denied);
|
|
2111
|
+
assert.throws(() => begin(denied, 'orchestrator', { paths: ['build.py'] }), /executable files/);
|
|
2112
|
+
});
|
|
2113
|
+
|
|
2114
|
+
test('intake can scan a supplied file before registration', (t) => {
|
|
2115
|
+
const f = useFixture(t);
|
|
2116
|
+
const sourceRoot = path.join(path.dirname(f.root), 'pre-registration-intake');
|
|
2117
|
+
fs.mkdirSync(sourceRoot, { recursive: true });
|
|
2118
|
+
t.after(() => fs.rmSync(sourceRoot, { force: true, recursive: true }));
|
|
2119
|
+
const reference = path.join(sourceRoot, 'reference.png');
|
|
2120
|
+
fs.writeFileSync(reference, PNG_1X1);
|
|
2121
|
+
const scan = f.harness.intake('orchestrator', { project: f.project, files: [reference], scan: true, generated: { tool: 'media-mcp', model: 'image-model', prompt: 'mood' } });
|
|
2122
|
+
assert.equal(scan.scan, true);
|
|
2123
|
+
assert.equal(scan.entries.length, 1);
|
|
2124
|
+
assert.match(scan.next, /Register/);
|
|
2125
|
+
});
|
|
2126
|
+
|
|
2127
|
+
test('intake copies references and external assets into the open change', (t) => {
|
|
2128
|
+
const f = useFixture(t);
|
|
2129
|
+
activate(f);
|
|
2130
|
+
begin(f);
|
|
2131
|
+
const sourceRoot = path.join(path.dirname(f.root), 'intake-source');
|
|
2132
|
+
fs.mkdirSync(sourceRoot, { recursive: true });
|
|
2133
|
+
t.after(() => fs.rmSync(sourceRoot, { force: true, recursive: true }));
|
|
2134
|
+
const reference = path.join(sourceRoot, 'mood board.png');
|
|
2135
|
+
const model = path.join(sourceRoot, 'chair.glb');
|
|
2136
|
+
fs.writeFileSync(reference, PNG_1X1);
|
|
2137
|
+
fs.writeFileSync(model, GLB);
|
|
2138
|
+
|
|
2139
|
+
const queued = f.harness.intake('orchestrator', {
|
|
2140
|
+
project: f.project,
|
|
2141
|
+
files: [reference],
|
|
2142
|
+
generated: { tool: 'media-mcp', model: 'image-model', prompt: 'warm desk', seed: '7' },
|
|
2143
|
+
});
|
|
2144
|
+
assert.equal(queued.copied.length, 1);
|
|
2145
|
+
assert.match(queued.copied[0].stored, /^Documents\/References\/mood-board-[a-f0-9]{8}\.png$/);
|
|
2146
|
+
assert.ok(fs.existsSync(path.join(f.root, f.project, queued.copied[0].stored)));
|
|
2147
|
+
assert.match(fs.readFileSync(path.join(f.root, f.project, 'Documents', 'References', 'README.md'), 'utf8'), /mood-board/);
|
|
2148
|
+
assert.match(fs.readFileSync(path.join(f.root, f.project, 'Documents', 'INTAKE.md'), 'utf8'), /mood-board/);
|
|
2149
|
+
|
|
2150
|
+
const asset = f.harness.intake('orchestrator', {
|
|
2151
|
+
project: f.project,
|
|
2152
|
+
files: [model],
|
|
2153
|
+
kind: 'model',
|
|
2154
|
+
source: { external: true, name: 'Desk chair', vendor: 'Example', author: 'Example', license: 'CC0-1.0', licenseText: 'CC0 text' },
|
|
2155
|
+
});
|
|
2156
|
+
assert.equal(asset.copied.length, 1);
|
|
2157
|
+
assert.equal(asset.copied[0].stored, 'assets/external/Example/Desk-chair/chair.glb');
|
|
2158
|
+
const directory = path.join(f.root, f.project, 'assets', 'external', 'Example', 'Desk-chair');
|
|
2159
|
+
assert.equal(fs.readFileSync(path.join(directory, 'LICENSE.txt'), 'utf8'), 'CC0 text');
|
|
2160
|
+
assert.match(fs.readFileSync(path.join(directory, 'SOURCE.md'), 'utf8'), /Desk chair/);
|
|
2161
|
+
assert.match(fs.readFileSync(path.join(f.root, f.project, 'Documents', 'ASSETS.md'), 'utf8'), /Desk-chair/);
|
|
2162
|
+
|
|
2163
|
+
const scan = f.harness.intake('orchestrator', { project: f.project, files: [reference], scan: true });
|
|
2164
|
+
assert.equal(scan.scan, true);
|
|
2165
|
+
assert.equal(scan.copied, undefined);
|
|
2166
|
+
assert.equal(scan.entries.length, 1);
|
|
2167
|
+
const status = f.harness.command('orchestrator', 'status');
|
|
2168
|
+
const change = status.change;
|
|
2169
|
+
assert.ok(change.paths.includes(`${f.project}/${queued.copied[0].stored}`));
|
|
2170
|
+
assert.ok(change.paths.includes(`${f.project}/Documents/INTAKE.md`));
|
|
2171
|
+
assert.equal(status.intake.copied, 1);
|
|
2172
|
+
assert.ok(status.intake.paths.includes(asset.copied[0].stored));
|
|
2173
|
+
});
|
|
2174
|
+
|
|
2175
|
+
test('reference-only intake closes as a non-scene change', (t) => {
|
|
2176
|
+
const f = useFixture(t);
|
|
2177
|
+
activate(f);
|
|
2178
|
+
begin(f, 'orchestrator', { paths: ['Documents/INTAKE.md'], final: false });
|
|
2179
|
+
const sourceRoot = path.join(path.dirname(f.root), 'reference-only-intake');
|
|
2180
|
+
fs.mkdirSync(sourceRoot, { recursive: true });
|
|
2181
|
+
t.after(() => fs.rmSync(sourceRoot, { force: true, recursive: true }));
|
|
2182
|
+
const reference = path.join(sourceRoot, 'mood.png');
|
|
2183
|
+
fs.writeFileSync(reference, PNG_1X1);
|
|
2184
|
+
f.harness.intake('orchestrator', { project: f.project, files: [reference], generated: { tool: 'media-mcp', model: 'image-model', prompt: 'mood' } });
|
|
2185
|
+
const change = f.harness.command('orchestrator', 'status').change;
|
|
2186
|
+
assert.equal(change.documentOnly, false);
|
|
2187
|
+
assert.equal(change.nonSceneOnly, true);
|
|
2188
|
+
f.harness.command('orchestrator', 'commit', { message: 'assets: add reference intake' });
|
|
2189
|
+
f.harness.command('orchestrator', 'checkpoint');
|
|
2190
|
+
assert.equal(f.harness.transaction((s) => s.projects[f.project].history.at(-1).verdict), 'DOCUMENT');
|
|
2191
|
+
});
|
|
2192
|
+
|
|
2193
|
+
test('only strict check_assets satisfies the asset receipt', (t) => {
|
|
2194
|
+
const strict = useFixture(t);
|
|
2195
|
+
activate(strict);
|
|
2196
|
+
begin(strict, 'orchestrator', { final: false });
|
|
2197
|
+
const strictIssue = issueAndConsume(strict, 'orchestrator', 'apply', { operation: 'check_assets', strict: true });
|
|
2198
|
+
strict.harness.finish(strictIssue.token, { ok: true, completed: 1, operationResults: [{ index: 1, operation: 'check_assets', ok: true }] });
|
|
2199
|
+
assert.equal(strict.harness.transaction((s) => s.projects[strict.project].change.assetChecked), true);
|
|
2200
|
+
const loose = useFixture(t);
|
|
2201
|
+
activate(loose);
|
|
2202
|
+
begin(loose, 'orchestrator', { final: false });
|
|
2203
|
+
const looseIssue = issueAndConsume(loose, 'orchestrator', 'apply', { operation: 'check_assets' });
|
|
2204
|
+
loose.harness.finish(looseIssue.token, { ok: true, completed: 1, operationResults: [{ index: 1, operation: 'check_assets', ok: true }] });
|
|
2205
|
+
assert.equal(loose.harness.transaction((s) => s.projects[loose.project].change.assetChecked), undefined);
|
|
2206
|
+
});
|
|
2207
|
+
|
|
2208
|
+
test('completion requires check_assets after an asset operation', (t) => {
|
|
2209
|
+
const f = useFixture(t);
|
|
2210
|
+
activate(f);
|
|
2211
|
+
begin(f);
|
|
2212
|
+
const issued = issueAndConsume(f, 'orchestrator', 'apply', { operation: 'import_asset', name: 'Desk', path: 'assets/models/desk.glb' });
|
|
2213
|
+
f.harness.finish(issued.token, { ok: true, completed: 1, operationResults: [{ index: 1, operation: 'import_asset', ok: true }] });
|
|
2214
|
+
seal(f);
|
|
2215
|
+
selfReview(f);
|
|
2216
|
+
scopedCommit(f);
|
|
2217
|
+
f.harness.command('orchestrator', 'checkpoint');
|
|
2218
|
+
assert.throws(() => f.harness.command('orchestrator', 'goal-complete'), /check_assets/);
|
|
2219
|
+
});
|