@weldr/runr 0.3.1 → 0.7.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/CHANGELOG.md +150 -1
- package/README.md +124 -111
- package/dist/audit/classifier.js +331 -0
- package/dist/cli.js +593 -282
- package/dist/commands/audit.js +259 -0
- package/dist/commands/bundle.js +180 -0
- package/dist/commands/continue.js +276 -0
- package/dist/commands/doctor.js +430 -45
- package/dist/commands/hooks.js +352 -0
- package/dist/commands/init.js +368 -8
- package/dist/commands/intervene.js +109 -0
- package/dist/commands/journal.js +167 -0
- package/dist/commands/meta.js +245 -0
- package/dist/commands/mode.js +157 -0
- package/dist/commands/orchestrate.js +29 -0
- package/dist/commands/packs.js +47 -0
- package/dist/commands/preflight.js +8 -5
- package/dist/commands/resume.js +421 -3
- package/dist/commands/run.js +63 -4
- package/dist/commands/status.js +47 -0
- package/dist/commands/submit.js +374 -0
- package/dist/config/schema.js +61 -1
- package/dist/diagnosis/analyzer.js +86 -1
- package/dist/diagnosis/formatter.js +3 -0
- package/dist/diagnosis/index.js +1 -0
- package/dist/diagnosis/stop-explainer.js +267 -0
- package/dist/diagnostics/stop-explainer.js +267 -0
- package/dist/guards/checkpoint.js +119 -0
- package/dist/journal/builder.js +497 -0
- package/dist/journal/redactor.js +68 -0
- package/dist/journal/renderer.js +220 -0
- package/dist/journal/types.js +7 -0
- package/dist/orchestrator/artifacts.js +17 -2
- package/dist/orchestrator/receipt.js +304 -0
- package/dist/output/stop-footer.js +185 -0
- package/dist/packs/actions.js +176 -0
- package/dist/packs/loader.js +200 -0
- package/dist/packs/renderer.js +46 -0
- package/dist/receipt/intervention.js +465 -0
- package/dist/receipt/writer.js +296 -0
- package/dist/redaction/redactor.js +95 -0
- package/dist/repo/context.js +147 -20
- package/dist/review/check-parser.js +211 -0
- package/dist/store/checkpoint-metadata.js +111 -0
- package/dist/store/run-store.js +21 -0
- package/dist/supervisor/runner.js +161 -10
- package/dist/tasks/task-metadata.js +74 -1
- package/dist/ux/brain.js +528 -0
- package/dist/ux/render.js +123 -0
- package/dist/ux/safe-commands.js +133 -0
- package/dist/ux/state.js +193 -0
- package/dist/ux/telemetry.js +110 -0
- package/package.json +5 -1
- package/packs/pr/pack.json +50 -0
- package/packs/pr/templates/AGENTS.md.tmpl +120 -0
- package/packs/pr/templates/CLAUDE.md.tmpl +101 -0
- package/packs/pr/templates/bundle.md.tmpl +27 -0
- package/packs/solo/pack.json +82 -0
- package/packs/solo/templates/AGENTS.md.tmpl +80 -0
- package/packs/solo/templates/CLAUDE.md.tmpl +126 -0
- package/packs/solo/templates/bundle.md.tmpl +27 -0
- package/packs/solo/templates/claude-cmd-bundle.md.tmpl +40 -0
- package/packs/solo/templates/claude-cmd-resume.md.tmpl +43 -0
- package/packs/solo/templates/claude-cmd-submit.md.tmpl +51 -0
- package/packs/solo/templates/claude-skill.md.tmpl +96 -0
- package/packs/trunk/pack.json +50 -0
- package/packs/trunk/templates/AGENTS.md.tmpl +87 -0
- package/packs/trunk/templates/CLAUDE.md.tmpl +126 -0
- package/packs/trunk/templates/bundle.md.tmpl +27 -0
- package/dist/commands/__tests__/report.test.js +0 -202
- package/dist/config/__tests__/presets.test.js +0 -104
- package/dist/context/__tests__/artifact.test.js +0 -130
- package/dist/context/__tests__/pack.test.js +0 -191
- package/dist/env/__tests__/fingerprint.test.js +0 -116
- package/dist/orchestrator/__tests__/policy.test.js +0 -185
- package/dist/orchestrator/__tests__/schema-version.test.js +0 -65
- package/dist/supervisor/__tests__/evidence-gate.test.js +0 -111
- package/dist/supervisor/__tests__/ownership.test.js +0 -103
- package/dist/supervisor/__tests__/state-machine.test.js +0 -290
- package/dist/workers/__tests__/claude.test.js +0 -88
- package/dist/workers/__tests__/codex.test.js +0 -81
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import os from 'node:os';
|
|
5
|
-
import { writeContextPackArtifact, readContextPackArtifact, formatContextPackStatus } from '../artifact.js';
|
|
6
|
-
describe('writeContextPackArtifact', () => {
|
|
7
|
-
let tempDir;
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'artifact-test-'));
|
|
10
|
-
});
|
|
11
|
-
afterEach(() => {
|
|
12
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
13
|
-
});
|
|
14
|
-
it('writes full pack when enabled', () => {
|
|
15
|
-
const pack = {
|
|
16
|
-
version: 1,
|
|
17
|
-
generated_at: '2025-12-24T01:00:00.000Z',
|
|
18
|
-
verification: {
|
|
19
|
-
tier0: ['pnpm lint'],
|
|
20
|
-
tier1: [],
|
|
21
|
-
tier2: []
|
|
22
|
-
},
|
|
23
|
-
reference_files: [],
|
|
24
|
-
scope: {
|
|
25
|
-
allowlist: ['src/**'],
|
|
26
|
-
denylist: []
|
|
27
|
-
},
|
|
28
|
-
patterns: {
|
|
29
|
-
tsconfig: null,
|
|
30
|
-
eslint: null,
|
|
31
|
-
package_json: null
|
|
32
|
-
},
|
|
33
|
-
blockers: {
|
|
34
|
-
scope_violations: [],
|
|
35
|
-
lockfile_restrictions: false,
|
|
36
|
-
common_errors: []
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
writeContextPackArtifact(tempDir, pack);
|
|
40
|
-
const artifactPath = path.join(tempDir, 'artifacts', 'context-pack.json');
|
|
41
|
-
expect(fs.existsSync(artifactPath)).toBe(true);
|
|
42
|
-
const content = JSON.parse(fs.readFileSync(artifactPath, 'utf-8'));
|
|
43
|
-
expect(content.enabled).toBe(true);
|
|
44
|
-
expect(content.pack_version).toBe(1);
|
|
45
|
-
expect(content.generated_at).toBe('2025-12-24T01:00:00.000Z');
|
|
46
|
-
expect(content.estimated_tokens).toBeGreaterThan(0);
|
|
47
|
-
expect(content.verification.tier0).toEqual(['pnpm lint']);
|
|
48
|
-
});
|
|
49
|
-
it('writes disabled stub when pack is null', () => {
|
|
50
|
-
writeContextPackArtifact(tempDir, null);
|
|
51
|
-
const artifactPath = path.join(tempDir, 'artifacts', 'context-pack.json');
|
|
52
|
-
expect(fs.existsSync(artifactPath)).toBe(true);
|
|
53
|
-
const content = JSON.parse(fs.readFileSync(artifactPath, 'utf-8'));
|
|
54
|
-
expect(content.enabled).toBe(false);
|
|
55
|
-
expect(content.pack_version).toBe(1);
|
|
56
|
-
expect(content.generated_at).toBeDefined();
|
|
57
|
-
expect(content.verification).toBeUndefined();
|
|
58
|
-
});
|
|
59
|
-
it('creates artifacts directory if missing', () => {
|
|
60
|
-
const artifactsDir = path.join(tempDir, 'artifacts');
|
|
61
|
-
expect(fs.existsSync(artifactsDir)).toBe(false);
|
|
62
|
-
writeContextPackArtifact(tempDir, null);
|
|
63
|
-
expect(fs.existsSync(artifactsDir)).toBe(true);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
describe('readContextPackArtifact', () => {
|
|
67
|
-
let tempDir;
|
|
68
|
-
beforeEach(() => {
|
|
69
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'artifact-test-'));
|
|
70
|
-
});
|
|
71
|
-
afterEach(() => {
|
|
72
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
73
|
-
});
|
|
74
|
-
it('returns null for missing file', () => {
|
|
75
|
-
const result = readContextPackArtifact(tempDir);
|
|
76
|
-
expect(result).toBeNull();
|
|
77
|
-
});
|
|
78
|
-
it('parses existing artifact', () => {
|
|
79
|
-
const artifactsDir = path.join(tempDir, 'artifacts');
|
|
80
|
-
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
81
|
-
const artifact = {
|
|
82
|
-
enabled: true,
|
|
83
|
-
pack_version: 1,
|
|
84
|
-
generated_at: '2025-12-24T01:00:00.000Z',
|
|
85
|
-
estimated_tokens: 500
|
|
86
|
-
};
|
|
87
|
-
fs.writeFileSync(path.join(artifactsDir, 'context-pack.json'), JSON.stringify(artifact));
|
|
88
|
-
const result = readContextPackArtifact(tempDir);
|
|
89
|
-
expect(result).not.toBeNull();
|
|
90
|
-
expect(result?.enabled).toBe(true);
|
|
91
|
-
expect(result?.estimated_tokens).toBe(500);
|
|
92
|
-
});
|
|
93
|
-
it('returns null for invalid JSON', () => {
|
|
94
|
-
const artifactsDir = path.join(tempDir, 'artifacts');
|
|
95
|
-
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
96
|
-
fs.writeFileSync(path.join(artifactsDir, 'context-pack.json'), 'not json');
|
|
97
|
-
const result = readContextPackArtifact(tempDir);
|
|
98
|
-
expect(result).toBeNull();
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
describe('formatContextPackStatus', () => {
|
|
102
|
-
it('formats null as not found', () => {
|
|
103
|
-
expect(formatContextPackStatus(null)).toBe('context_pack: (not found)');
|
|
104
|
-
});
|
|
105
|
-
it('formats disabled artifact', () => {
|
|
106
|
-
const artifact = {
|
|
107
|
-
enabled: false,
|
|
108
|
-
pack_version: 1,
|
|
109
|
-
generated_at: '2025-12-24T01:00:00.000Z'
|
|
110
|
-
};
|
|
111
|
-
expect(formatContextPackStatus(artifact)).toBe('context_pack: disabled');
|
|
112
|
-
});
|
|
113
|
-
it('formats enabled artifact with tokens', () => {
|
|
114
|
-
const artifact = {
|
|
115
|
-
enabled: true,
|
|
116
|
-
pack_version: 1,
|
|
117
|
-
generated_at: '2025-12-24T01:00:00.000Z',
|
|
118
|
-
estimated_tokens: 493
|
|
119
|
-
};
|
|
120
|
-
expect(formatContextPackStatus(artifact)).toBe('context_pack: present (493 tokens)');
|
|
121
|
-
});
|
|
122
|
-
it('handles missing token count', () => {
|
|
123
|
-
const artifact = {
|
|
124
|
-
enabled: true,
|
|
125
|
-
pack_version: 1,
|
|
126
|
-
generated_at: '2025-12-24T01:00:00.000Z'
|
|
127
|
-
};
|
|
128
|
-
expect(formatContextPackStatus(artifact)).toBe('context_pack: present (? tokens)');
|
|
129
|
-
});
|
|
130
|
-
});
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { buildContextPack, formatContextPackForPrompt, estimatePackTokens } from '../pack.js';
|
|
4
|
-
// Use the actual repo root for integration-style tests
|
|
5
|
-
const REPO_ROOT = path.resolve(__dirname, '../../..');
|
|
6
|
-
describe('buildContextPack', () => {
|
|
7
|
-
describe('verification commands', () => {
|
|
8
|
-
it('extracts verification commands from config', () => {
|
|
9
|
-
const pack = buildContextPack({
|
|
10
|
-
repoRoot: REPO_ROOT,
|
|
11
|
-
targetRoot: 'apps/tactical-grid',
|
|
12
|
-
config: {
|
|
13
|
-
verification: {
|
|
14
|
-
tier0: ['pnpm lint', 'pnpm typecheck'],
|
|
15
|
-
tier1: ['pnpm test'],
|
|
16
|
-
tier2: []
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
expect(pack.verification.tier0).toEqual(['pnpm lint', 'pnpm typecheck']);
|
|
21
|
-
expect(pack.verification.tier1).toEqual(['pnpm test']);
|
|
22
|
-
expect(pack.verification.tier2).toEqual([]);
|
|
23
|
-
});
|
|
24
|
-
it('handles missing verification config', () => {
|
|
25
|
-
const pack = buildContextPack({
|
|
26
|
-
repoRoot: REPO_ROOT,
|
|
27
|
-
targetRoot: 'apps/tactical-grid',
|
|
28
|
-
config: {}
|
|
29
|
-
});
|
|
30
|
-
expect(pack.verification.tier0).toEqual([]);
|
|
31
|
-
expect(pack.verification.tier1).toEqual([]);
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
describe('reference files', () => {
|
|
35
|
-
it('resolves RNG pattern reference', () => {
|
|
36
|
-
const pack = buildContextPack({
|
|
37
|
-
repoRoot: REPO_ROOT,
|
|
38
|
-
targetRoot: 'apps/tactical-grid',
|
|
39
|
-
config: {},
|
|
40
|
-
references: [{ pattern: 'RNG pattern from deckbuilder' }]
|
|
41
|
-
});
|
|
42
|
-
expect(pack.reference_files.length).toBeGreaterThan(0);
|
|
43
|
-
const rngRef = pack.reference_files.find((r) => r.path.includes('rng.ts'));
|
|
44
|
-
expect(rngRef).toBeDefined();
|
|
45
|
-
expect(rngRef?.content).toContain('nextInt');
|
|
46
|
-
expect(rngRef?.content).toContain('1103515245'); // LCG constant
|
|
47
|
-
});
|
|
48
|
-
it('resolves explicit hint path', () => {
|
|
49
|
-
const pack = buildContextPack({
|
|
50
|
-
repoRoot: REPO_ROOT,
|
|
51
|
-
targetRoot: 'apps/tactical-grid',
|
|
52
|
-
config: {},
|
|
53
|
-
references: [
|
|
54
|
-
{
|
|
55
|
-
pattern: 'custom reference',
|
|
56
|
-
hint: 'apps/deckbuilder/src/engine/rng.ts'
|
|
57
|
-
}
|
|
58
|
-
]
|
|
59
|
-
});
|
|
60
|
-
expect(pack.reference_files.length).toBe(1);
|
|
61
|
-
expect(pack.reference_files[0].path).toBe('apps/deckbuilder/src/engine/rng.ts');
|
|
62
|
-
expect(pack.reference_files[0].reason).toBe('custom reference');
|
|
63
|
-
});
|
|
64
|
-
it('handles unknown pattern gracefully', () => {
|
|
65
|
-
const pack = buildContextPack({
|
|
66
|
-
repoRoot: REPO_ROOT,
|
|
67
|
-
targetRoot: 'apps/tactical-grid',
|
|
68
|
-
config: {},
|
|
69
|
-
references: [{ pattern: 'nonexistent magic pattern' }]
|
|
70
|
-
});
|
|
71
|
-
expect(pack.reference_files).toEqual([]);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
describe('scope constraints', () => {
|
|
75
|
-
it('extracts scope from config', () => {
|
|
76
|
-
const pack = buildContextPack({
|
|
77
|
-
repoRoot: REPO_ROOT,
|
|
78
|
-
targetRoot: 'apps/tactical-grid',
|
|
79
|
-
config: {
|
|
80
|
-
scope: {
|
|
81
|
-
allowlist: ['apps/tactical-grid/**'],
|
|
82
|
-
denylist: ['**/node_modules/**']
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
expect(pack.scope.allowlist).toEqual(['apps/tactical-grid/**']);
|
|
87
|
-
expect(pack.scope.denylist).toEqual(['**/node_modules/**']);
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
describe('config patterns', () => {
|
|
91
|
-
it('finds nearest tsconfig.json', () => {
|
|
92
|
-
const pack = buildContextPack({
|
|
93
|
-
repoRoot: REPO_ROOT,
|
|
94
|
-
targetRoot: 'apps/deckbuilder',
|
|
95
|
-
config: {}
|
|
96
|
-
});
|
|
97
|
-
expect(pack.patterns.tsconfig).not.toBeNull();
|
|
98
|
-
expect(pack.patterns.tsconfig?.path).toContain('tsconfig.json');
|
|
99
|
-
expect(pack.patterns.tsconfig?.content).toContain('compilerOptions');
|
|
100
|
-
});
|
|
101
|
-
it('finds nearest eslint config', () => {
|
|
102
|
-
const pack = buildContextPack({
|
|
103
|
-
repoRoot: REPO_ROOT,
|
|
104
|
-
targetRoot: 'apps/deckbuilder',
|
|
105
|
-
config: {}
|
|
106
|
-
});
|
|
107
|
-
expect(pack.patterns.eslint).not.toBeNull();
|
|
108
|
-
expect(pack.patterns.eslint?.path).toMatch(/eslint\.config/);
|
|
109
|
-
});
|
|
110
|
-
it('finds nearest package.json', () => {
|
|
111
|
-
const pack = buildContextPack({
|
|
112
|
-
repoRoot: REPO_ROOT,
|
|
113
|
-
targetRoot: 'apps/deckbuilder',
|
|
114
|
-
config: {}
|
|
115
|
-
});
|
|
116
|
-
expect(pack.patterns.package_json).not.toBeNull();
|
|
117
|
-
expect(pack.patterns.package_json?.content).toContain('scripts');
|
|
118
|
-
});
|
|
119
|
-
it('finds config even for nonexistent target (upward search or fallback)', () => {
|
|
120
|
-
const pack = buildContextPack({
|
|
121
|
-
repoRoot: REPO_ROOT,
|
|
122
|
-
targetRoot: 'apps/nonexistent-app',
|
|
123
|
-
config: {}
|
|
124
|
-
});
|
|
125
|
-
// Should find config via upward search (repo root) or fallback to deckbuilder
|
|
126
|
-
expect(pack.patterns.tsconfig).not.toBeNull();
|
|
127
|
-
expect(pack.patterns.tsconfig?.content).toContain('compilerOptions');
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
describe('version and metadata', () => {
|
|
131
|
-
it('includes version 1', () => {
|
|
132
|
-
const pack = buildContextPack({
|
|
133
|
-
repoRoot: REPO_ROOT,
|
|
134
|
-
targetRoot: 'apps/tactical-grid',
|
|
135
|
-
config: {}
|
|
136
|
-
});
|
|
137
|
-
expect(pack.version).toBe(1);
|
|
138
|
-
});
|
|
139
|
-
it('includes generated_at timestamp', () => {
|
|
140
|
-
const pack = buildContextPack({
|
|
141
|
-
repoRoot: REPO_ROOT,
|
|
142
|
-
targetRoot: 'apps/tactical-grid',
|
|
143
|
-
config: {}
|
|
144
|
-
});
|
|
145
|
-
expect(pack.generated_at).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
describe('formatContextPackForPrompt', () => {
|
|
150
|
-
it('formats verification commands', () => {
|
|
151
|
-
const pack = buildContextPack({
|
|
152
|
-
repoRoot: REPO_ROOT,
|
|
153
|
-
targetRoot: 'apps/tactical-grid',
|
|
154
|
-
config: {
|
|
155
|
-
verification: {
|
|
156
|
-
tier0: ['pnpm lint', 'pnpm typecheck'],
|
|
157
|
-
tier1: ['pnpm test']
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
const formatted = formatContextPackForPrompt(pack);
|
|
162
|
-
expect(formatted).toContain('Verification Commands');
|
|
163
|
-
expect(formatted).toContain('tier0: pnpm lint && pnpm typecheck');
|
|
164
|
-
expect(formatted).toContain('tier1: pnpm test');
|
|
165
|
-
});
|
|
166
|
-
it('includes reference file content', () => {
|
|
167
|
-
const pack = buildContextPack({
|
|
168
|
-
repoRoot: REPO_ROOT,
|
|
169
|
-
targetRoot: 'apps/tactical-grid',
|
|
170
|
-
config: {},
|
|
171
|
-
references: [{ pattern: 'RNG pattern' }]
|
|
172
|
-
});
|
|
173
|
-
const formatted = formatContextPackForPrompt(pack);
|
|
174
|
-
expect(formatted).toContain('Reference Files');
|
|
175
|
-
expect(formatted).toContain('nextInt');
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
describe('estimatePackTokens', () => {
|
|
179
|
-
it('estimates token count based on character length', () => {
|
|
180
|
-
const pack = buildContextPack({
|
|
181
|
-
repoRoot: REPO_ROOT,
|
|
182
|
-
targetRoot: 'apps/tactical-grid',
|
|
183
|
-
config: {
|
|
184
|
-
verification: { tier0: ['pnpm lint'] }
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
const tokens = estimatePackTokens(pack);
|
|
188
|
-
expect(tokens).toBeGreaterThan(0);
|
|
189
|
-
expect(tokens).toBeLessThan(10000); // Sanity check
|
|
190
|
-
});
|
|
191
|
-
});
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { compareFingerprints } from '../fingerprint.js';
|
|
3
|
-
function makeFingerprint(overrides = {}) {
|
|
4
|
-
return {
|
|
5
|
-
node_version: 'v20.0.0',
|
|
6
|
-
package_manager: 'npm',
|
|
7
|
-
lockfile_hash: 'abc123def456',
|
|
8
|
-
worker_versions: {
|
|
9
|
-
codex: 'codex-cli 0.70.0',
|
|
10
|
-
claude: '2.0.50 (Claude Code)'
|
|
11
|
-
},
|
|
12
|
-
created_at: '2025-01-01T00:00:00.000Z',
|
|
13
|
-
...overrides
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
describe('compareFingerprints', () => {
|
|
17
|
-
it('returns empty array when fingerprints match', () => {
|
|
18
|
-
const original = makeFingerprint();
|
|
19
|
-
const current = makeFingerprint();
|
|
20
|
-
const diffs = compareFingerprints(original, current);
|
|
21
|
-
expect(diffs).toEqual([]);
|
|
22
|
-
});
|
|
23
|
-
it('detects node version change', () => {
|
|
24
|
-
const original = makeFingerprint({ node_version: 'v20.0.0' });
|
|
25
|
-
const current = makeFingerprint({ node_version: 'v22.0.0' });
|
|
26
|
-
const diffs = compareFingerprints(original, current);
|
|
27
|
-
expect(diffs).toHaveLength(1);
|
|
28
|
-
expect(diffs[0]).toEqual({
|
|
29
|
-
field: 'node_version',
|
|
30
|
-
original: 'v20.0.0',
|
|
31
|
-
current: 'v22.0.0'
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
it('detects package manager change', () => {
|
|
35
|
-
const original = makeFingerprint({ package_manager: 'npm' });
|
|
36
|
-
const current = makeFingerprint({ package_manager: 'pnpm' });
|
|
37
|
-
const diffs = compareFingerprints(original, current);
|
|
38
|
-
expect(diffs).toHaveLength(1);
|
|
39
|
-
expect(diffs[0].field).toBe('package_manager');
|
|
40
|
-
});
|
|
41
|
-
it('detects lockfile hash change', () => {
|
|
42
|
-
const original = makeFingerprint({ lockfile_hash: 'abc123' });
|
|
43
|
-
const current = makeFingerprint({ lockfile_hash: 'def456' });
|
|
44
|
-
const diffs = compareFingerprints(original, current);
|
|
45
|
-
expect(diffs).toHaveLength(1);
|
|
46
|
-
expect(diffs[0].field).toBe('lockfile_hash');
|
|
47
|
-
});
|
|
48
|
-
it('detects worker version change', () => {
|
|
49
|
-
const original = makeFingerprint({
|
|
50
|
-
worker_versions: { codex: '0.70.0', claude: '2.0.50' }
|
|
51
|
-
});
|
|
52
|
-
const current = makeFingerprint({
|
|
53
|
-
worker_versions: { codex: '0.80.0', claude: '2.0.50' }
|
|
54
|
-
});
|
|
55
|
-
const diffs = compareFingerprints(original, current);
|
|
56
|
-
expect(diffs).toHaveLength(1);
|
|
57
|
-
expect(diffs[0].field).toBe('worker:codex');
|
|
58
|
-
expect(diffs[0].original).toBe('0.70.0');
|
|
59
|
-
expect(diffs[0].current).toBe('0.80.0');
|
|
60
|
-
});
|
|
61
|
-
it('handles null lockfile gracefully', () => {
|
|
62
|
-
const original = makeFingerprint({ lockfile_hash: null });
|
|
63
|
-
const current = makeFingerprint({ lockfile_hash: 'abc123' });
|
|
64
|
-
const diffs = compareFingerprints(original, current);
|
|
65
|
-
expect(diffs).toHaveLength(1);
|
|
66
|
-
expect(diffs[0].field).toBe('lockfile_hash');
|
|
67
|
-
expect(diffs[0].original).toBeNull();
|
|
68
|
-
});
|
|
69
|
-
it('handles null package manager gracefully', () => {
|
|
70
|
-
const original = makeFingerprint({ package_manager: null });
|
|
71
|
-
const current = makeFingerprint({ package_manager: null });
|
|
72
|
-
const diffs = compareFingerprints(original, current);
|
|
73
|
-
expect(diffs).toEqual([]);
|
|
74
|
-
});
|
|
75
|
-
it('handles null worker version gracefully', () => {
|
|
76
|
-
const original = makeFingerprint({
|
|
77
|
-
worker_versions: { codex: null, claude: '2.0.50' }
|
|
78
|
-
});
|
|
79
|
-
const current = makeFingerprint({
|
|
80
|
-
worker_versions: { codex: '0.80.0', claude: '2.0.50' }
|
|
81
|
-
});
|
|
82
|
-
const diffs = compareFingerprints(original, current);
|
|
83
|
-
expect(diffs).toHaveLength(1);
|
|
84
|
-
expect(diffs[0].original).toBeNull();
|
|
85
|
-
expect(diffs[0].current).toBe('0.80.0');
|
|
86
|
-
});
|
|
87
|
-
it('detects multiple changes at once', () => {
|
|
88
|
-
const original = makeFingerprint({
|
|
89
|
-
node_version: 'v20.0.0',
|
|
90
|
-
lockfile_hash: 'old-hash',
|
|
91
|
-
worker_versions: { codex: '0.70.0', claude: '2.0.50' }
|
|
92
|
-
});
|
|
93
|
-
const current = makeFingerprint({
|
|
94
|
-
node_version: 'v22.0.0',
|
|
95
|
-
lockfile_hash: 'new-hash',
|
|
96
|
-
worker_versions: { codex: '0.80.0', claude: '2.0.50' }
|
|
97
|
-
});
|
|
98
|
-
const diffs = compareFingerprints(original, current);
|
|
99
|
-
expect(diffs).toHaveLength(3);
|
|
100
|
-
expect(diffs.map((d) => d.field).sort()).toEqual([
|
|
101
|
-
'lockfile_hash',
|
|
102
|
-
'node_version',
|
|
103
|
-
'worker:codex'
|
|
104
|
-
]);
|
|
105
|
-
});
|
|
106
|
-
it('ignores created_at timestamp differences', () => {
|
|
107
|
-
const original = makeFingerprint({
|
|
108
|
-
created_at: '2025-01-01T00:00:00.000Z'
|
|
109
|
-
});
|
|
110
|
-
const current = makeFingerprint({
|
|
111
|
-
created_at: '2025-06-15T12:30:00.000Z'
|
|
112
|
-
});
|
|
113
|
-
const diffs = compareFingerprints(original, current);
|
|
114
|
-
expect(diffs).toEqual([]);
|
|
115
|
-
});
|
|
116
|
-
});
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Policy block tests for Phase 7B.
|
|
3
|
-
*
|
|
4
|
-
* These tests verify:
|
|
5
|
-
* 1. Run creates state.policy correctly from CLI/config
|
|
6
|
-
* 2. Resume without overrides keeps policy unchanged (via getEffectivePolicy)
|
|
7
|
-
* 3. Legacy states (without policy block) are handled correctly
|
|
8
|
-
*/
|
|
9
|
-
import { describe, it, expect } from 'vitest';
|
|
10
|
-
import { createInitialOrchestratorState, getEffectivePolicy } from '../state-machine.js';
|
|
11
|
-
// Sample config for testing
|
|
12
|
-
const sampleConfig = {
|
|
13
|
-
tracks: [
|
|
14
|
-
{
|
|
15
|
-
name: 'Track A',
|
|
16
|
-
steps: [{ task: 'tasks/a.md' }]
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
name: 'Track B',
|
|
20
|
-
steps: [{ task: 'tasks/b.md' }]
|
|
21
|
-
}
|
|
22
|
-
]
|
|
23
|
-
};
|
|
24
|
-
describe('Policy Block', () => {
|
|
25
|
-
describe('createInitialOrchestratorState', () => {
|
|
26
|
-
it('creates state.policy correctly from CLI options', () => {
|
|
27
|
-
const state = createInitialOrchestratorState(sampleConfig, '/test/repo', {
|
|
28
|
-
timeBudgetMinutes: 60,
|
|
29
|
-
maxTicks: 25,
|
|
30
|
-
collisionPolicy: 'serialize',
|
|
31
|
-
fast: true,
|
|
32
|
-
autoResume: true,
|
|
33
|
-
parallel: 1,
|
|
34
|
-
ownershipRequired: true
|
|
35
|
-
});
|
|
36
|
-
// Policy block should exist
|
|
37
|
-
expect(state.policy).toBeDefined();
|
|
38
|
-
// Policy values should match options
|
|
39
|
-
expect(state.policy.time_budget_minutes).toBe(60);
|
|
40
|
-
expect(state.policy.max_ticks).toBe(25);
|
|
41
|
-
expect(state.policy.collision_policy).toBe('serialize');
|
|
42
|
-
expect(state.policy.fast).toBe(true);
|
|
43
|
-
expect(state.policy.auto_resume).toBe(true);
|
|
44
|
-
expect(state.policy.parallel).toBe(1);
|
|
45
|
-
expect(state.policy.ownership_required).toBe(true);
|
|
46
|
-
});
|
|
47
|
-
it('sets default values for optional policy fields', () => {
|
|
48
|
-
const state = createInitialOrchestratorState(sampleConfig, '/test/repo', {
|
|
49
|
-
timeBudgetMinutes: 120,
|
|
50
|
-
maxTicks: 50,
|
|
51
|
-
collisionPolicy: 'force'
|
|
52
|
-
// fast, autoResume, parallel not provided
|
|
53
|
-
});
|
|
54
|
-
expect(state.policy).toBeDefined();
|
|
55
|
-
expect(state.policy.fast).toBe(false);
|
|
56
|
-
expect(state.policy.auto_resume).toBe(false);
|
|
57
|
-
expect(state.policy.parallel).toBe(2); // Default: track count
|
|
58
|
-
expect(state.policy.ownership_required).toBe(false);
|
|
59
|
-
});
|
|
60
|
-
it('writes both policy block and legacy fields for backward compatibility', () => {
|
|
61
|
-
const state = createInitialOrchestratorState(sampleConfig, '/test/repo', {
|
|
62
|
-
timeBudgetMinutes: 90,
|
|
63
|
-
maxTicks: 30,
|
|
64
|
-
collisionPolicy: 'fail',
|
|
65
|
-
fast: true
|
|
66
|
-
});
|
|
67
|
-
// Policy block
|
|
68
|
-
expect(state.policy.time_budget_minutes).toBe(90);
|
|
69
|
-
expect(state.policy.max_ticks).toBe(30);
|
|
70
|
-
expect(state.policy.collision_policy).toBe('fail');
|
|
71
|
-
expect(state.policy.fast).toBe(true);
|
|
72
|
-
// Legacy fields (should match)
|
|
73
|
-
expect(state.time_budget_minutes).toBe(90);
|
|
74
|
-
expect(state.max_ticks).toBe(30);
|
|
75
|
-
expect(state.collision_policy).toBe('fail');
|
|
76
|
-
expect(state.fast).toBe(true);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
describe('getEffectivePolicy', () => {
|
|
80
|
-
it('returns policy block when present', () => {
|
|
81
|
-
const state = createInitialOrchestratorState(sampleConfig, '/test/repo', {
|
|
82
|
-
timeBudgetMinutes: 45,
|
|
83
|
-
maxTicks: 15,
|
|
84
|
-
collisionPolicy: 'serialize',
|
|
85
|
-
fast: true,
|
|
86
|
-
autoResume: true
|
|
87
|
-
});
|
|
88
|
-
const policy = getEffectivePolicy(state);
|
|
89
|
-
expect(policy.time_budget_minutes).toBe(45);
|
|
90
|
-
expect(policy.max_ticks).toBe(15);
|
|
91
|
-
expect(policy.collision_policy).toBe('serialize');
|
|
92
|
-
expect(policy.fast).toBe(true);
|
|
93
|
-
expect(policy.auto_resume).toBe(true);
|
|
94
|
-
});
|
|
95
|
-
it('falls back to legacy fields when policy block is missing (v0 state)', () => {
|
|
96
|
-
// Simulate a legacy v0 state without policy block
|
|
97
|
-
const legacyState = {
|
|
98
|
-
orchestrator_id: 'orch20240101120000',
|
|
99
|
-
repo_path: '/test/repo',
|
|
100
|
-
tracks: [
|
|
101
|
-
{
|
|
102
|
-
id: 'track-1',
|
|
103
|
-
name: 'Track A',
|
|
104
|
-
steps: [{ task_path: 'tasks/a.md' }],
|
|
105
|
-
current_step: 0,
|
|
106
|
-
status: 'pending'
|
|
107
|
-
}
|
|
108
|
-
],
|
|
109
|
-
active_runs: {},
|
|
110
|
-
file_claims: {},
|
|
111
|
-
status: 'running',
|
|
112
|
-
started_at: '2024-01-01T12:00:00Z',
|
|
113
|
-
// No policy block - legacy v0 state
|
|
114
|
-
collision_policy: 'force',
|
|
115
|
-
time_budget_minutes: 180,
|
|
116
|
-
max_ticks: 100,
|
|
117
|
-
fast: true
|
|
118
|
-
};
|
|
119
|
-
const policy = getEffectivePolicy(legacyState);
|
|
120
|
-
// Should extract from legacy fields
|
|
121
|
-
expect(policy.time_budget_minutes).toBe(180);
|
|
122
|
-
expect(policy.max_ticks).toBe(100);
|
|
123
|
-
expect(policy.collision_policy).toBe('force');
|
|
124
|
-
expect(policy.fast).toBe(true);
|
|
125
|
-
// Defaults for fields not in v0
|
|
126
|
-
expect(policy.auto_resume).toBe(false);
|
|
127
|
-
expect(policy.parallel).toBe(1); // track count
|
|
128
|
-
});
|
|
129
|
-
it('handles legacy state with fast=undefined', () => {
|
|
130
|
-
const legacyState = {
|
|
131
|
-
orchestrator_id: 'orch20240101120000',
|
|
132
|
-
repo_path: '/test/repo',
|
|
133
|
-
tracks: [],
|
|
134
|
-
active_runs: {},
|
|
135
|
-
file_claims: {},
|
|
136
|
-
status: 'running',
|
|
137
|
-
started_at: '2024-01-01T12:00:00Z',
|
|
138
|
-
collision_policy: 'serialize',
|
|
139
|
-
time_budget_minutes: 60,
|
|
140
|
-
max_ticks: 25
|
|
141
|
-
// fast is undefined (missing in v0)
|
|
142
|
-
};
|
|
143
|
-
const policy = getEffectivePolicy(legacyState);
|
|
144
|
-
expect(policy.fast).toBe(false); // Default when undefined
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
describe('Resume policy immutability', () => {
|
|
148
|
-
it('resume without overrides keeps policy unchanged', () => {
|
|
149
|
-
// Create initial state
|
|
150
|
-
const state = createInitialOrchestratorState(sampleConfig, '/test/repo', {
|
|
151
|
-
timeBudgetMinutes: 60,
|
|
152
|
-
maxTicks: 25,
|
|
153
|
-
collisionPolicy: 'serialize',
|
|
154
|
-
fast: true
|
|
155
|
-
});
|
|
156
|
-
// Simulate "resuming" by getting effective policy
|
|
157
|
-
const policyBeforeResume = getEffectivePolicy(state);
|
|
158
|
-
const policyAfterResume = getEffectivePolicy(state);
|
|
159
|
-
// Policy should be identical
|
|
160
|
-
expect(policyAfterResume).toEqual(policyBeforeResume);
|
|
161
|
-
});
|
|
162
|
-
it('policy values remain stable across multiple reads', () => {
|
|
163
|
-
const state = createInitialOrchestratorState(sampleConfig, '/test/repo', {
|
|
164
|
-
timeBudgetMinutes: 42,
|
|
165
|
-
maxTicks: 17,
|
|
166
|
-
collisionPolicy: 'force',
|
|
167
|
-
fast: false,
|
|
168
|
-
autoResume: true
|
|
169
|
-
});
|
|
170
|
-
// Read policy multiple times
|
|
171
|
-
const policy1 = getEffectivePolicy(state);
|
|
172
|
-
const policy2 = getEffectivePolicy(state);
|
|
173
|
-
const policy3 = getEffectivePolicy(state);
|
|
174
|
-
// All reads should return identical values
|
|
175
|
-
expect(policy1).toEqual(policy2);
|
|
176
|
-
expect(policy2).toEqual(policy3);
|
|
177
|
-
// And match original options
|
|
178
|
-
expect(policy1.time_budget_minutes).toBe(42);
|
|
179
|
-
expect(policy1.max_ticks).toBe(17);
|
|
180
|
-
expect(policy1.collision_policy).toBe('force');
|
|
181
|
-
expect(policy1.fast).toBe(false);
|
|
182
|
-
expect(policy1.auto_resume).toBe(true);
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
});
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Schema version tests for Phase 7C.
|
|
3
|
-
*
|
|
4
|
-
* These tests verify:
|
|
5
|
-
* 1. Artifacts include schema_version field
|
|
6
|
-
* 2. Schema version is the expected value
|
|
7
|
-
*/
|
|
8
|
-
import { describe, it, expect } from 'vitest';
|
|
9
|
-
import { buildWaitResult, buildSummaryArtifact, buildStopArtifact } from '../artifacts.js';
|
|
10
|
-
import { createInitialOrchestratorState } from '../state-machine.js';
|
|
11
|
-
import { ORCHESTRATOR_ARTIFACT_SCHEMA_VERSION } from '../types.js';
|
|
12
|
-
const sampleConfig = {
|
|
13
|
-
tracks: [
|
|
14
|
-
{
|
|
15
|
-
name: 'Test Track',
|
|
16
|
-
steps: [{ task: 'tasks/test.md' }]
|
|
17
|
-
}
|
|
18
|
-
]
|
|
19
|
-
};
|
|
20
|
-
describe('Schema Versioning', () => {
|
|
21
|
-
describe('Orchestration Artifacts', () => {
|
|
22
|
-
it('buildWaitResult includes schema_version', () => {
|
|
23
|
-
const state = createInitialOrchestratorState(sampleConfig, '/test/repo', {
|
|
24
|
-
timeBudgetMinutes: 60,
|
|
25
|
-
maxTicks: 25,
|
|
26
|
-
collisionPolicy: 'serialize'
|
|
27
|
-
});
|
|
28
|
-
// Mark as complete for testing
|
|
29
|
-
state.status = 'complete';
|
|
30
|
-
state.ended_at = new Date().toISOString();
|
|
31
|
-
const result = buildWaitResult(state, '/test/repo');
|
|
32
|
-
expect(result.schema_version).toBe(ORCHESTRATOR_ARTIFACT_SCHEMA_VERSION);
|
|
33
|
-
expect(result.schema_version).toBe(1);
|
|
34
|
-
});
|
|
35
|
-
it('buildSummaryArtifact includes schema_version', () => {
|
|
36
|
-
const state = createInitialOrchestratorState(sampleConfig, '/test/repo', {
|
|
37
|
-
timeBudgetMinutes: 60,
|
|
38
|
-
maxTicks: 25,
|
|
39
|
-
collisionPolicy: 'serialize'
|
|
40
|
-
});
|
|
41
|
-
state.status = 'complete';
|
|
42
|
-
state.ended_at = new Date().toISOString();
|
|
43
|
-
const summary = buildSummaryArtifact(state, '/test/repo');
|
|
44
|
-
expect(summary.schema_version).toBe(ORCHESTRATOR_ARTIFACT_SCHEMA_VERSION);
|
|
45
|
-
expect(summary.schema_version).toBe(1);
|
|
46
|
-
});
|
|
47
|
-
it('buildStopArtifact includes schema_version', () => {
|
|
48
|
-
const state = createInitialOrchestratorState(sampleConfig, '/test/repo', {
|
|
49
|
-
timeBudgetMinutes: 60,
|
|
50
|
-
maxTicks: 25,
|
|
51
|
-
collisionPolicy: 'serialize'
|
|
52
|
-
});
|
|
53
|
-
state.status = 'stopped';
|
|
54
|
-
state.ended_at = new Date().toISOString();
|
|
55
|
-
const stopArtifact = buildStopArtifact(state, '/test/repo');
|
|
56
|
-
expect(stopArtifact.schema_version).toBe(ORCHESTRATOR_ARTIFACT_SCHEMA_VERSION);
|
|
57
|
-
expect(stopArtifact.schema_version).toBe(1);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
describe('Schema version constant', () => {
|
|
61
|
-
it('ORCHESTRATOR_ARTIFACT_SCHEMA_VERSION is 1', () => {
|
|
62
|
-
expect(ORCHESTRATOR_ARTIFACT_SCHEMA_VERSION).toBe(1);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
});
|