@soleri/core 9.10.0 → 9.12.1
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/dist/adapters/types.d.ts +2 -0
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/brain/brain.d.ts +5 -1
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +97 -10
- package/dist/brain/brain.js.map +1 -1
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +4 -0
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/brain/types.d.ts +1 -1
- package/dist/brain/types.d.ts.map +1 -1
- package/dist/dream/cron-manager.d.ts +10 -0
- package/dist/dream/cron-manager.d.ts.map +1 -0
- package/dist/dream/cron-manager.js +122 -0
- package/dist/dream/cron-manager.js.map +1 -0
- package/dist/dream/dream-engine.d.ts +34 -0
- package/dist/dream/dream-engine.d.ts.map +1 -0
- package/dist/dream/dream-engine.js +88 -0
- package/dist/dream/dream-engine.js.map +1 -0
- package/dist/dream/dream-ops.d.ts +8 -0
- package/dist/dream/dream-ops.d.ts.map +1 -0
- package/dist/dream/dream-ops.js +49 -0
- package/dist/dream/dream-ops.js.map +1 -0
- package/dist/dream/index.d.ts +7 -0
- package/dist/dream/index.d.ts.map +1 -0
- package/dist/dream/index.js +5 -0
- package/dist/dream/index.js.map +1 -0
- package/dist/dream/schema.d.ts +3 -0
- package/dist/dream/schema.d.ts.map +1 -0
- package/dist/dream/schema.js +16 -0
- package/dist/dream/schema.js.map +1 -0
- package/dist/embeddings/index.d.ts +5 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +3 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/embeddings/openai-provider.d.ts +31 -0
- package/dist/embeddings/openai-provider.d.ts.map +1 -0
- package/dist/embeddings/openai-provider.js +120 -0
- package/dist/embeddings/openai-provider.js.map +1 -0
- package/dist/embeddings/pipeline.d.ts +36 -0
- package/dist/embeddings/pipeline.d.ts.map +1 -0
- package/dist/embeddings/pipeline.js +78 -0
- package/dist/embeddings/pipeline.js.map +1 -0
- package/dist/embeddings/types.d.ts +62 -0
- package/dist/embeddings/types.d.ts.map +1 -0
- package/dist/embeddings/types.js +3 -0
- package/dist/embeddings/types.js.map +1 -0
- package/dist/engine/bin/soleri-engine.js +4 -1
- package/dist/engine/bin/soleri-engine.js.map +1 -1
- package/dist/engine/module-manifest.d.ts.map +1 -1
- package/dist/engine/module-manifest.js +20 -0
- package/dist/engine/module-manifest.js.map +1 -1
- package/dist/engine/register-engine.d.ts.map +1 -1
- package/dist/engine/register-engine.js +12 -0
- package/dist/engine/register-engine.js.map +1 -1
- package/dist/flows/chain-types.d.ts +8 -8
- package/dist/flows/dispatch-registry.d.ts +15 -1
- package/dist/flows/dispatch-registry.d.ts.map +1 -1
- package/dist/flows/dispatch-registry.js +28 -1
- package/dist/flows/dispatch-registry.js.map +1 -1
- package/dist/flows/executor.d.ts +20 -2
- package/dist/flows/executor.d.ts.map +1 -1
- package/dist/flows/executor.js +79 -1
- package/dist/flows/executor.js.map +1 -1
- package/dist/flows/index.d.ts +2 -1
- package/dist/flows/index.d.ts.map +1 -1
- package/dist/flows/index.js.map +1 -1
- package/dist/flows/types.d.ts +43 -21
- package/dist/flows/types.d.ts.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/planning/plan-lifecycle.d.ts.map +1 -1
- package/dist/planning/plan-lifecycle.js +4 -2
- package/dist/planning/plan-lifecycle.js.map +1 -1
- package/dist/planning/planner-types.d.ts +1 -1
- package/dist/planning/planner-types.d.ts.map +1 -1
- package/dist/plugins/types.d.ts +31 -31
- package/dist/runtime/admin-ops.d.ts.map +1 -1
- package/dist/runtime/admin-ops.js +15 -0
- package/dist/runtime/admin-ops.js.map +1 -1
- package/dist/runtime/admin-setup-ops.js +2 -2
- package/dist/runtime/admin-setup-ops.js.map +1 -1
- package/dist/runtime/embedding-ops.d.ts +12 -0
- package/dist/runtime/embedding-ops.d.ts.map +1 -0
- package/dist/runtime/embedding-ops.js +96 -0
- package/dist/runtime/embedding-ops.js.map +1 -0
- package/dist/runtime/facades/embedding-facade.d.ts +7 -0
- package/dist/runtime/facades/embedding-facade.d.ts.map +1 -0
- package/dist/runtime/facades/embedding-facade.js +8 -0
- package/dist/runtime/facades/embedding-facade.js.map +1 -0
- package/dist/runtime/facades/index.d.ts.map +1 -1
- package/dist/runtime/facades/index.js +12 -0
- package/dist/runtime/facades/index.js.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.js +120 -0
- package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
- package/dist/runtime/feature-flags.d.ts.map +1 -1
- package/dist/runtime/feature-flags.js +4 -0
- package/dist/runtime/feature-flags.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +146 -12
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
- package/dist/runtime/planning-extra-ops.js +51 -0
- package/dist/runtime/planning-extra-ops.js.map +1 -1
- package/dist/runtime/preflight.d.ts +32 -0
- package/dist/runtime/preflight.d.ts.map +1 -0
- package/dist/runtime/preflight.js +29 -0
- package/dist/runtime/preflight.js.map +1 -0
- package/dist/runtime/quality-signals.d.ts +6 -1
- package/dist/runtime/quality-signals.d.ts.map +1 -1
- package/dist/runtime/quality-signals.js +41 -5
- package/dist/runtime/quality-signals.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +33 -2
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/types.d.ts +27 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/skills/step-tracker.d.ts +39 -0
- package/dist/skills/step-tracker.d.ts.map +1 -0
- package/dist/skills/step-tracker.js +105 -0
- package/dist/skills/step-tracker.js.map +1 -0
- package/dist/skills/sync-skills.d.ts +3 -2
- package/dist/skills/sync-skills.d.ts.map +1 -1
- package/dist/skills/sync-skills.js +42 -8
- package/dist/skills/sync-skills.js.map +1 -1
- package/dist/subagent/dispatcher.d.ts +4 -3
- package/dist/subagent/dispatcher.d.ts.map +1 -1
- package/dist/subagent/dispatcher.js +57 -35
- package/dist/subagent/dispatcher.js.map +1 -1
- package/dist/subagent/index.d.ts +1 -0
- package/dist/subagent/index.d.ts.map +1 -1
- package/dist/subagent/index.js.map +1 -1
- package/dist/subagent/orphan-reaper.d.ts +51 -4
- package/dist/subagent/orphan-reaper.d.ts.map +1 -1
- package/dist/subagent/orphan-reaper.js +103 -3
- package/dist/subagent/orphan-reaper.js.map +1 -1
- package/dist/subagent/types.d.ts +7 -0
- package/dist/subagent/types.d.ts.map +1 -1
- package/dist/subagent/workspace-resolver.d.ts +2 -0
- package/dist/subagent/workspace-resolver.d.ts.map +1 -1
- package/dist/subagent/workspace-resolver.js +3 -1
- package/dist/subagent/workspace-resolver.js.map +1 -1
- package/dist/vault/vault-entries.d.ts +18 -0
- package/dist/vault/vault-entries.d.ts.map +1 -1
- package/dist/vault/vault-entries.js +73 -0
- package/dist/vault/vault-entries.js.map +1 -1
- package/dist/vault/vault-manager.d.ts.map +1 -1
- package/dist/vault/vault-manager.js +1 -0
- package/dist/vault/vault-manager.js.map +1 -1
- package/dist/vault/vault-schema.d.ts.map +1 -1
- package/dist/vault/vault-schema.js +14 -0
- package/dist/vault/vault-schema.js.map +1 -1
- package/dist/vault/vault.d.ts +1 -0
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js.map +1 -1
- package/package.json +3 -5
- package/src/__tests__/cron-manager.test.ts +132 -0
- package/src/__tests__/deviation-detection.test.ts +234 -0
- package/src/__tests__/embeddings.test.ts +536 -0
- package/src/__tests__/preflight.test.ts +97 -0
- package/src/__tests__/step-persistence.test.ts +324 -0
- package/src/__tests__/step-tracker.test.ts +260 -0
- package/src/__tests__/subagent/dispatcher.test.ts +122 -4
- package/src/__tests__/subagent/orphan-reaper.test.ts +148 -12
- package/src/__tests__/subagent/process-lifecycle.test.ts +422 -0
- package/src/__tests__/subagent/workspace-resolver.test.ts +6 -1
- package/src/adapters/types.ts +2 -0
- package/src/brain/brain.ts +117 -9
- package/src/brain/intelligence.ts +4 -0
- package/src/brain/types.ts +6 -1
- package/src/dream/cron-manager.ts +137 -0
- package/src/dream/dream-engine.ts +119 -0
- package/src/dream/dream-ops.ts +56 -0
- package/src/dream/dream.test.ts +182 -0
- package/src/dream/index.ts +6 -0
- package/src/dream/schema.ts +17 -0
- package/src/embeddings/openai-provider.ts +158 -0
- package/src/embeddings/pipeline.ts +126 -0
- package/src/embeddings/types.ts +67 -0
- package/src/engine/bin/soleri-engine.ts +4 -1
- package/src/engine/module-manifest.test.ts +4 -4
- package/src/engine/module-manifest.ts +20 -0
- package/src/engine/register-engine.ts +12 -0
- package/src/flows/dispatch-registry.ts +44 -1
- package/src/flows/executor.ts +93 -2
- package/src/flows/index.ts +2 -0
- package/src/flows/types.ts +39 -1
- package/src/index.ts +11 -0
- package/src/planning/goal-ancestry.test.ts +3 -5
- package/src/planning/plan-lifecycle.ts +5 -2
- package/src/planning/planner-types.ts +1 -1
- package/src/planning/planner.test.ts +73 -3
- package/src/runtime/admin-ops.test.ts +2 -2
- package/src/runtime/admin-ops.ts +17 -0
- package/src/runtime/admin-setup-ops.ts +2 -2
- package/src/runtime/embedding-ops.ts +116 -0
- package/src/runtime/facades/admin-facade.test.ts +31 -0
- package/src/runtime/facades/embedding-facade.ts +11 -0
- package/src/runtime/facades/index.ts +12 -0
- package/src/runtime/facades/orchestrate-facade.test.ts +16 -0
- package/src/runtime/facades/orchestrate-facade.ts +146 -0
- package/src/runtime/feature-flags.ts +4 -0
- package/src/runtime/orchestrate-ops.test.ts +182 -2
- package/src/runtime/orchestrate-ops.ts +170 -13
- package/src/runtime/planning-extra-ops.ts +77 -0
- package/src/runtime/preflight.ts +53 -0
- package/src/runtime/quality-signals.test.ts +182 -8
- package/src/runtime/quality-signals.ts +44 -5
- package/src/runtime/runtime.ts +41 -2
- package/src/runtime/types.ts +20 -0
- package/src/skills/__tests__/sync-skills.test.ts +132 -0
- package/src/skills/step-tracker.ts +162 -0
- package/src/skills/sync-skills.ts +54 -9
- package/src/subagent/dispatcher.ts +62 -39
- package/src/subagent/index.ts +1 -0
- package/src/subagent/orphan-reaper.test.ts +135 -0
- package/src/subagent/orphan-reaper.ts +130 -7
- package/src/subagent/types.ts +10 -0
- package/src/subagent/workspace-resolver.ts +3 -1
- package/src/vault/vault-entries.ts +112 -0
- package/src/vault/vault-manager.ts +1 -0
- package/src/vault/vault-scaling.test.ts +3 -2
- package/src/vault/vault-schema.ts +15 -0
- package/src/vault/vault.ts +1 -0
- package/vitest.config.ts +2 -1
- package/dist/brain/strength-scorer.d.ts +0 -31
- package/dist/brain/strength-scorer.d.ts.map +0 -1
- package/dist/brain/strength-scorer.js +0 -264
- package/dist/brain/strength-scorer.js.map +0 -1
- package/dist/engine/index.d.ts +0 -21
- package/dist/engine/index.d.ts.map +0 -1
- package/dist/engine/index.js +0 -18
- package/dist/engine/index.js.map +0 -1
- package/dist/hooks/index.d.ts +0 -2
- package/dist/hooks/index.d.ts.map +0 -1
- package/dist/hooks/index.js +0 -2
- package/dist/hooks/index.js.map +0 -1
- package/dist/persona/index.d.ts +0 -5
- package/dist/persona/index.d.ts.map +0 -1
- package/dist/persona/index.js +0 -4
- package/dist/persona/index.js.map +0 -1
- package/dist/vault/vault-interfaces.d.ts +0 -153
- package/dist/vault/vault-interfaces.d.ts.map +0 -1
- package/dist/vault/vault-interfaces.js +0 -2
- package/dist/vault/vault-interfaces.js.map +0 -1
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step Persistence — Unit Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for the incremental correction protocol:
|
|
5
|
+
* - Step output persistence to disk
|
|
6
|
+
* - Manifest management (load/save/create)
|
|
7
|
+
* - Rerun marking and staleness propagation
|
|
8
|
+
* - cascadeTo behavior
|
|
9
|
+
* - rerunCount incrementing
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import { getPlanRunDir, loadManifest, saveManifest, persistStepOutput } from '../flows/executor.js';
|
|
17
|
+
import type { PlanRunManifest, StepState } from '../flows/types.js';
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Helpers
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
let tmpDir: string;
|
|
24
|
+
|
|
25
|
+
function createTmpDir(): string {
|
|
26
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'soleri-step-persist-'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function cleanTmpDir(dir: string): void {
|
|
30
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Build a manifest with N completed steps for testing. */
|
|
34
|
+
function buildManifest(planId: string, stepCount: number): PlanRunManifest {
|
|
35
|
+
const now = new Date().toISOString();
|
|
36
|
+
const steps: Record<string, StepState> = {};
|
|
37
|
+
for (let i = 0; i < stepCount; i++) {
|
|
38
|
+
steps[`step-${i}`] = {
|
|
39
|
+
status: 'completed',
|
|
40
|
+
output: { result: `output-${i}` },
|
|
41
|
+
timestamp: now,
|
|
42
|
+
rerunCount: 0,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return { planId, steps, lastRun: now, createdAt: now };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Tests
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
describe('Step Persistence', () => {
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
tmpDir = createTmpDir();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
cleanTmpDir(tmpDir);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ─── getPlanRunDir ─────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
describe('getPlanRunDir', () => {
|
|
64
|
+
it('returns the correct path structure', () => {
|
|
65
|
+
const dir = getPlanRunDir('/project', 'plan-123');
|
|
66
|
+
expect(dir).toBe(path.join('/project', '.soleri', 'plan-runs', 'plan-123'));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('handles plan IDs with special characters', () => {
|
|
70
|
+
const dir = getPlanRunDir('/project', 'plan_abc-def');
|
|
71
|
+
expect(dir).toContain('plan_abc-def');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ─── loadManifest ──────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
describe('loadManifest', () => {
|
|
78
|
+
it('creates a fresh manifest when none exists', () => {
|
|
79
|
+
const runDir = path.join(tmpDir, 'nonexistent');
|
|
80
|
+
const manifest = loadManifest(runDir, 'plan-new');
|
|
81
|
+
|
|
82
|
+
expect(manifest.planId).toBe('plan-new');
|
|
83
|
+
expect(manifest.steps).toEqual({});
|
|
84
|
+
expect(manifest.createdAt).toBeTruthy();
|
|
85
|
+
expect(manifest.lastRun).toBeTruthy();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('loads existing manifest from disk', () => {
|
|
89
|
+
const runDir = path.join(tmpDir, 'existing');
|
|
90
|
+
const original = buildManifest('plan-load', 3);
|
|
91
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
92
|
+
fs.writeFileSync(path.join(runDir, 'manifest.json'), JSON.stringify(original));
|
|
93
|
+
|
|
94
|
+
const loaded = loadManifest(runDir, 'plan-load');
|
|
95
|
+
expect(loaded.planId).toBe('plan-load');
|
|
96
|
+
expect(Object.keys(loaded.steps)).toHaveLength(3);
|
|
97
|
+
expect(loaded.steps['step-0'].status).toBe('completed');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ─── saveManifest ──────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
describe('saveManifest', () => {
|
|
104
|
+
it('creates directories and writes manifest', () => {
|
|
105
|
+
const runDir = path.join(tmpDir, 'deep', 'nested', 'dir');
|
|
106
|
+
const manifest = buildManifest('plan-save', 2);
|
|
107
|
+
|
|
108
|
+
saveManifest(runDir, manifest);
|
|
109
|
+
|
|
110
|
+
const manifestPath = path.join(runDir, 'manifest.json');
|
|
111
|
+
expect(fs.existsSync(manifestPath)).toBe(true);
|
|
112
|
+
|
|
113
|
+
const written = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
114
|
+
expect(written.planId).toBe('plan-save');
|
|
115
|
+
expect(Object.keys(written.steps)).toHaveLength(2);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('overwrites existing manifest', () => {
|
|
119
|
+
const runDir = path.join(tmpDir, 'overwrite');
|
|
120
|
+
const manifest1 = buildManifest('plan-ow', 1);
|
|
121
|
+
saveManifest(runDir, manifest1);
|
|
122
|
+
|
|
123
|
+
const manifest2 = buildManifest('plan-ow', 3);
|
|
124
|
+
saveManifest(runDir, manifest2);
|
|
125
|
+
|
|
126
|
+
const loaded = loadManifest(runDir, 'plan-ow');
|
|
127
|
+
expect(Object.keys(loaded.steps)).toHaveLength(3);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ─── persistStepOutput ─────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
describe('persistStepOutput', () => {
|
|
134
|
+
it('persists step output file and updates manifest', () => {
|
|
135
|
+
const runDir = path.join(tmpDir, 'persist-step');
|
|
136
|
+
const manifest = buildManifest('plan-ps', 0);
|
|
137
|
+
|
|
138
|
+
persistStepOutput(runDir, manifest, 0, 'init', { data: 'hello' });
|
|
139
|
+
|
|
140
|
+
// Step file should exist
|
|
141
|
+
const stepFile = path.join(runDir, 'step-0-init.json');
|
|
142
|
+
expect(fs.existsSync(stepFile)).toBe(true);
|
|
143
|
+
const stepData = JSON.parse(fs.readFileSync(stepFile, 'utf-8'));
|
|
144
|
+
expect(stepData.data).toBe('hello');
|
|
145
|
+
|
|
146
|
+
// Manifest should be updated
|
|
147
|
+
expect(manifest.steps['init']).toBeDefined();
|
|
148
|
+
expect(manifest.steps['init'].status).toBe('completed');
|
|
149
|
+
expect(manifest.steps['init'].rerunCount).toBe(0);
|
|
150
|
+
|
|
151
|
+
// Manifest file should exist on disk
|
|
152
|
+
const manifestFile = path.join(runDir, 'manifest.json');
|
|
153
|
+
expect(fs.existsSync(manifestFile)).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('increments rerunCount when step already exists', () => {
|
|
157
|
+
const runDir = path.join(tmpDir, 'persist-rerun');
|
|
158
|
+
const manifest = buildManifest('plan-pr', 0);
|
|
159
|
+
|
|
160
|
+
// First run
|
|
161
|
+
persistStepOutput(runDir, manifest, 0, 'step-a', { v: 1 });
|
|
162
|
+
expect(manifest.steps['step-a'].rerunCount).toBe(0);
|
|
163
|
+
|
|
164
|
+
// Second run — rerunCount should increment
|
|
165
|
+
persistStepOutput(runDir, manifest, 0, 'step-a', { v: 2 });
|
|
166
|
+
expect(manifest.steps['step-a'].rerunCount).toBe(1);
|
|
167
|
+
|
|
168
|
+
// Third run
|
|
169
|
+
persistStepOutput(runDir, manifest, 0, 'step-a', { v: 3 });
|
|
170
|
+
expect(manifest.steps['step-a'].rerunCount).toBe(2);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('preserves rerunReason from previous state', () => {
|
|
174
|
+
const runDir = path.join(tmpDir, 'persist-reason');
|
|
175
|
+
const manifest = buildManifest('plan-reason', 0);
|
|
176
|
+
|
|
177
|
+
// Manually set a step with a rerun reason
|
|
178
|
+
manifest.steps['step-x'] = {
|
|
179
|
+
status: 'rerun',
|
|
180
|
+
output: null,
|
|
181
|
+
timestamp: new Date().toISOString(),
|
|
182
|
+
rerunCount: 1,
|
|
183
|
+
rerunReason: 'gate failed',
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
persistStepOutput(runDir, manifest, 0, 'step-x', { fixed: true });
|
|
187
|
+
expect(manifest.steps['step-x'].rerunReason).toBe('gate failed');
|
|
188
|
+
expect(manifest.steps['step-x'].rerunCount).toBe(2);
|
|
189
|
+
expect(manifest.steps['step-x'].status).toBe('completed');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ─── Rerun marking and staleness propagation ───────────────────
|
|
194
|
+
|
|
195
|
+
describe('rerun marking and staleness propagation', () => {
|
|
196
|
+
it('marks target step as rerun and downstream as stale', () => {
|
|
197
|
+
const manifest = buildManifest('plan-stale', 5);
|
|
198
|
+
|
|
199
|
+
// Simulate what orchestrate_rerun_step does
|
|
200
|
+
const stepNumber = 1;
|
|
201
|
+
const reason = 'output was wrong';
|
|
202
|
+
const now = new Date().toISOString();
|
|
203
|
+
const sortedStepIds = Object.keys(manifest.steps);
|
|
204
|
+
|
|
205
|
+
for (let i = 0; i < sortedStepIds.length; i++) {
|
|
206
|
+
const sid = sortedStepIds[i];
|
|
207
|
+
const state = manifest.steps[sid];
|
|
208
|
+
|
|
209
|
+
if (i === stepNumber) {
|
|
210
|
+
state.status = 'rerun';
|
|
211
|
+
state.rerunCount += 1;
|
|
212
|
+
state.rerunReason = reason;
|
|
213
|
+
state.timestamp = now;
|
|
214
|
+
} else if (i > stepNumber) {
|
|
215
|
+
state.status = 'stale';
|
|
216
|
+
state.timestamp = now;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
expect(manifest.steps['step-0'].status).toBe('completed');
|
|
221
|
+
expect(manifest.steps['step-1'].status).toBe('rerun');
|
|
222
|
+
expect(manifest.steps['step-1'].rerunCount).toBe(1);
|
|
223
|
+
expect(manifest.steps['step-1'].rerunReason).toBe('output was wrong');
|
|
224
|
+
expect(manifest.steps['step-2'].status).toBe('stale');
|
|
225
|
+
expect(manifest.steps['step-3'].status).toBe('stale');
|
|
226
|
+
expect(manifest.steps['step-4'].status).toBe('stale');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('cascadeTo marks a range as rerun instead of stale', () => {
|
|
230
|
+
const manifest = buildManifest('plan-cascade', 5);
|
|
231
|
+
|
|
232
|
+
const stepNumber = 1;
|
|
233
|
+
const cascadeTo = 4; // steps 1,2,3 → rerun; step 4 → stale
|
|
234
|
+
const reason = 'dependency changed';
|
|
235
|
+
const now = new Date().toISOString();
|
|
236
|
+
const sortedStepIds = Object.keys(manifest.steps);
|
|
237
|
+
|
|
238
|
+
for (let i = 0; i < sortedStepIds.length; i++) {
|
|
239
|
+
const sid = sortedStepIds[i];
|
|
240
|
+
const state = manifest.steps[sid];
|
|
241
|
+
|
|
242
|
+
if (i === stepNumber) {
|
|
243
|
+
state.status = 'rerun';
|
|
244
|
+
state.rerunCount += 1;
|
|
245
|
+
state.rerunReason = reason;
|
|
246
|
+
state.timestamp = now;
|
|
247
|
+
} else if (i > stepNumber) {
|
|
248
|
+
if (i < cascadeTo) {
|
|
249
|
+
state.status = 'rerun';
|
|
250
|
+
state.rerunCount += 1;
|
|
251
|
+
state.rerunReason = `Cascade from step ${stepNumber}: ${reason}`;
|
|
252
|
+
state.timestamp = now;
|
|
253
|
+
} else {
|
|
254
|
+
state.status = 'stale';
|
|
255
|
+
state.timestamp = now;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
expect(manifest.steps['step-0'].status).toBe('completed');
|
|
261
|
+
expect(manifest.steps['step-1'].status).toBe('rerun');
|
|
262
|
+
expect(manifest.steps['step-1'].rerunCount).toBe(1);
|
|
263
|
+
expect(manifest.steps['step-2'].status).toBe('rerun');
|
|
264
|
+
expect(manifest.steps['step-2'].rerunCount).toBe(1);
|
|
265
|
+
expect(manifest.steps['step-3'].status).toBe('rerun');
|
|
266
|
+
expect(manifest.steps['step-3'].rerunCount).toBe(1);
|
|
267
|
+
expect(manifest.steps['step-4'].status).toBe('stale');
|
|
268
|
+
expect(manifest.steps['step-4'].rerunCount).toBe(0);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('rerunCount accumulates across multiple reruns', () => {
|
|
272
|
+
const manifest = buildManifest('plan-multi', 3);
|
|
273
|
+
|
|
274
|
+
// First rerun of step 1
|
|
275
|
+
manifest.steps['step-1'].status = 'rerun';
|
|
276
|
+
manifest.steps['step-1'].rerunCount += 1;
|
|
277
|
+
|
|
278
|
+
// Second rerun of step 1
|
|
279
|
+
manifest.steps['step-1'].rerunCount += 1;
|
|
280
|
+
|
|
281
|
+
// Third rerun
|
|
282
|
+
manifest.steps['step-1'].rerunCount += 1;
|
|
283
|
+
|
|
284
|
+
expect(manifest.steps['step-1'].rerunCount).toBe(3);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// ─── Round-trip persistence ────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
describe('round-trip persistence', () => {
|
|
291
|
+
it('manifest survives save and load cycle', () => {
|
|
292
|
+
const runDir = path.join(tmpDir, 'roundtrip');
|
|
293
|
+
const manifest = buildManifest('plan-rt', 3);
|
|
294
|
+
|
|
295
|
+
// Mark step 1 as rerun
|
|
296
|
+
manifest.steps['step-1'].status = 'rerun';
|
|
297
|
+
manifest.steps['step-1'].rerunCount = 2;
|
|
298
|
+
manifest.steps['step-1'].rerunReason = 'test reason';
|
|
299
|
+
|
|
300
|
+
saveManifest(runDir, manifest);
|
|
301
|
+
const loaded = loadManifest(runDir, 'plan-rt');
|
|
302
|
+
|
|
303
|
+
expect(loaded.steps['step-1'].status).toBe('rerun');
|
|
304
|
+
expect(loaded.steps['step-1'].rerunCount).toBe(2);
|
|
305
|
+
expect(loaded.steps['step-1'].rerunReason).toBe('test reason');
|
|
306
|
+
expect(loaded.steps['step-0'].status).toBe('completed');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('step output files persist alongside manifest', () => {
|
|
310
|
+
const runDir = path.join(tmpDir, 'files');
|
|
311
|
+
const manifest = buildManifest('plan-files', 0);
|
|
312
|
+
|
|
313
|
+
persistStepOutput(runDir, manifest, 0, 'alpha', { x: 1 });
|
|
314
|
+
persistStepOutput(runDir, manifest, 1, 'beta', { x: 2 });
|
|
315
|
+
persistStepOutput(runDir, manifest, 2, 'gamma', { x: 3 });
|
|
316
|
+
|
|
317
|
+
const files = fs.readdirSync(runDir).sort();
|
|
318
|
+
expect(files).toContain('manifest.json');
|
|
319
|
+
expect(files).toContain('step-0-alpha.json');
|
|
320
|
+
expect(files).toContain('step-1-beta.json');
|
|
321
|
+
expect(files).toContain('step-2-gamma.json');
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
});
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import {
|
|
6
|
+
createTracker,
|
|
7
|
+
advanceStep,
|
|
8
|
+
recordEvidence,
|
|
9
|
+
generateCheckpoint,
|
|
10
|
+
validateCompletion,
|
|
11
|
+
persistTracker,
|
|
12
|
+
loadTracker,
|
|
13
|
+
} from '../skills/step-tracker.js';
|
|
14
|
+
import type { SkillStep, SkillStepTracker } from '../skills/step-tracker.js';
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Fixtures
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
const SAMPLE_STEPS: SkillStep[] = [
|
|
21
|
+
{ id: 'vault-query', description: 'Query vault for relevant patterns', evidence: 'tool_called' },
|
|
22
|
+
{ id: 'analysis', description: 'Run analysis', evidence: 'file_exists' },
|
|
23
|
+
{ id: 'apply-fix', description: 'Apply the fix', evidence: 'tool_called' },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// createTracker
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
describe('createTracker', () => {
|
|
31
|
+
it('creates tracker with correct initial state', () => {
|
|
32
|
+
const tracker = createTracker('test-skill', SAMPLE_STEPS);
|
|
33
|
+
|
|
34
|
+
expect(tracker.skillName).toBe('test-skill');
|
|
35
|
+
expect(tracker.runId).toMatch(/^test-skill-\d+$/);
|
|
36
|
+
expect(tracker.steps).toEqual(SAMPLE_STEPS);
|
|
37
|
+
expect(tracker.currentStep).toBe(0);
|
|
38
|
+
expect(tracker.startedAt).toBeTruthy();
|
|
39
|
+
expect(tracker.evidence).toEqual({});
|
|
40
|
+
expect(tracker.completedAt).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// advanceStep
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
describe('advanceStep', () => {
|
|
49
|
+
it('increments currentStep', () => {
|
|
50
|
+
const tracker = createTracker('test', SAMPLE_STEPS);
|
|
51
|
+
const advanced = advanceStep(tracker);
|
|
52
|
+
|
|
53
|
+
expect(advanced.currentStep).toBe(1);
|
|
54
|
+
expect(advanced.completedAt).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('on last step sets completedAt', () => {
|
|
58
|
+
let tracker = createTracker('test', SAMPLE_STEPS);
|
|
59
|
+
// Advance to last step (index 2)
|
|
60
|
+
tracker = { ...tracker, currentStep: SAMPLE_STEPS.length - 1 };
|
|
61
|
+
const completed = advanceStep(tracker);
|
|
62
|
+
|
|
63
|
+
expect(completed.completedAt).toBeTruthy();
|
|
64
|
+
// currentStep stays at last index
|
|
65
|
+
expect(completed.currentStep).toBe(SAMPLE_STEPS.length - 1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('does not mutate original tracker', () => {
|
|
69
|
+
const tracker = createTracker('test', SAMPLE_STEPS);
|
|
70
|
+
advanceStep(tracker);
|
|
71
|
+
|
|
72
|
+
expect(tracker.currentStep).toBe(0);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// recordEvidence
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
describe('recordEvidence', () => {
|
|
81
|
+
it('stores evidence for a step', () => {
|
|
82
|
+
const tracker = createTracker('test', SAMPLE_STEPS);
|
|
83
|
+
const updated = recordEvidence(tracker, 'vault-query', 'vault.search called');
|
|
84
|
+
|
|
85
|
+
expect(updated.evidence['vault-query']).toBeDefined();
|
|
86
|
+
expect(updated.evidence['vault-query'].type).toBe('tool_called');
|
|
87
|
+
expect(updated.evidence['vault-query'].value).toBe('vault.search called');
|
|
88
|
+
expect(updated.evidence['vault-query'].verified).toBe(true);
|
|
89
|
+
expect(updated.evidence['vault-query'].timestamp).toBeTruthy();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('ignores unknown step IDs', () => {
|
|
93
|
+
const tracker = createTracker('test', SAMPLE_STEPS);
|
|
94
|
+
const updated = recordEvidence(tracker, 'nonexistent', 'some value');
|
|
95
|
+
|
|
96
|
+
expect(updated).toBe(tracker); // same reference — no change
|
|
97
|
+
expect(Object.keys(updated.evidence)).toHaveLength(0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('respects verified parameter', () => {
|
|
101
|
+
const tracker = createTracker('test', SAMPLE_STEPS);
|
|
102
|
+
const updated = recordEvidence(tracker, 'vault-query', 'value', false);
|
|
103
|
+
|
|
104
|
+
expect(updated.evidence['vault-query'].verified).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// generateCheckpoint
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
describe('generateCheckpoint', () => {
|
|
113
|
+
it('formats correctly with 0 completions', () => {
|
|
114
|
+
const tracker = createTracker('my-skill', SAMPLE_STEPS);
|
|
115
|
+
const cp = generateCheckpoint(tracker);
|
|
116
|
+
|
|
117
|
+
expect(cp).toContain('--- Skill Checkpoint: my-skill ---');
|
|
118
|
+
expect(cp).toContain('Completed: none');
|
|
119
|
+
expect(cp).toContain('Current: vault-query (step 1 of 3)');
|
|
120
|
+
expect(cp).toContain('Evidence required: tool_called');
|
|
121
|
+
expect(cp).toContain('Progress: 0/3');
|
|
122
|
+
expect(cp).toContain('---');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('formats correctly with partial completion', () => {
|
|
126
|
+
let tracker = createTracker('my-skill', SAMPLE_STEPS);
|
|
127
|
+
tracker = recordEvidence(tracker, 'vault-query', 'called');
|
|
128
|
+
tracker = advanceStep(tracker);
|
|
129
|
+
const cp = generateCheckpoint(tracker);
|
|
130
|
+
|
|
131
|
+
expect(cp).toContain('vault-query ✓');
|
|
132
|
+
expect(cp).toContain('Current: analysis (step 2 of 3)');
|
|
133
|
+
expect(cp).toContain('Progress: 1/3');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('formats correctly with full completion', () => {
|
|
137
|
+
let tracker = createTracker('my-skill', SAMPLE_STEPS);
|
|
138
|
+
tracker = recordEvidence(tracker, 'vault-query', 'called');
|
|
139
|
+
tracker = recordEvidence(tracker, 'analysis', '/tmp/result.json');
|
|
140
|
+
tracker = recordEvidence(tracker, 'apply-fix', 'fix.apply called');
|
|
141
|
+
// Advance past last step
|
|
142
|
+
tracker = { ...tracker, currentStep: SAMPLE_STEPS.length - 1 };
|
|
143
|
+
tracker = advanceStep(tracker);
|
|
144
|
+
const cp = generateCheckpoint(tracker);
|
|
145
|
+
|
|
146
|
+
expect(cp).toContain('vault-query ✓, analysis ✓, apply-fix ✓');
|
|
147
|
+
expect(cp).toContain('Progress: 3/3');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// validateCompletion
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
describe('validateCompletion', () => {
|
|
156
|
+
it('returns correct skipped steps when none completed', () => {
|
|
157
|
+
const tracker = createTracker('test', SAMPLE_STEPS);
|
|
158
|
+
const result = validateCompletion(tracker);
|
|
159
|
+
|
|
160
|
+
expect(result.complete).toBe(false);
|
|
161
|
+
expect(result.skippedSteps).toEqual(['vault-query', 'analysis', 'apply-fix']);
|
|
162
|
+
expect(result.evidenceCount).toBe(0);
|
|
163
|
+
expect(result.totalSteps).toBe(3);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('returns complete=true when all steps have verified evidence', () => {
|
|
167
|
+
let tracker = createTracker('test', SAMPLE_STEPS);
|
|
168
|
+
tracker = recordEvidence(tracker, 'vault-query', 'v');
|
|
169
|
+
tracker = recordEvidence(tracker, 'analysis', 'a');
|
|
170
|
+
tracker = recordEvidence(tracker, 'apply-fix', 'f');
|
|
171
|
+
const result = validateCompletion(tracker);
|
|
172
|
+
|
|
173
|
+
expect(result.complete).toBe(true);
|
|
174
|
+
expect(result.skippedSteps).toEqual([]);
|
|
175
|
+
expect(result.evidenceCount).toBe(3);
|
|
176
|
+
expect(result.totalSteps).toBe(3);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('does not count unverified evidence as complete', () => {
|
|
180
|
+
let tracker = createTracker('test', SAMPLE_STEPS);
|
|
181
|
+
tracker = recordEvidence(tracker, 'vault-query', 'v', false);
|
|
182
|
+
tracker = recordEvidence(tracker, 'analysis', 'a', true);
|
|
183
|
+
tracker = recordEvidence(tracker, 'apply-fix', 'f', true);
|
|
184
|
+
const result = validateCompletion(tracker);
|
|
185
|
+
|
|
186
|
+
expect(result.complete).toBe(false);
|
|
187
|
+
expect(result.skippedSteps).toEqual(['vault-query']);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// persistTracker / loadTracker
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
describe('persistence', () => {
|
|
196
|
+
let tmpDir: string;
|
|
197
|
+
let originalHome: string | undefined;
|
|
198
|
+
let originalUserProfile: string | undefined;
|
|
199
|
+
|
|
200
|
+
beforeEach(() => {
|
|
201
|
+
tmpDir = join(tmpdir(), `step-tracker-test-${Date.now()}`);
|
|
202
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
203
|
+
originalHome = process.env.HOME;
|
|
204
|
+
originalUserProfile = process.env.USERPROFILE;
|
|
205
|
+
// Override HOME (Unix) and USERPROFILE (Windows) so getRunsDir() writes to our temp dir
|
|
206
|
+
process.env.HOME = tmpDir;
|
|
207
|
+
process.env.USERPROFILE = tmpDir;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
afterEach(() => {
|
|
211
|
+
process.env.HOME = originalHome;
|
|
212
|
+
process.env.USERPROFILE = originalUserProfile;
|
|
213
|
+
try {
|
|
214
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
215
|
+
} catch {
|
|
216
|
+
// cleanup best-effort
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('round-trips correctly', () => {
|
|
221
|
+
let tracker = createTracker('persist-test', SAMPLE_STEPS);
|
|
222
|
+
tracker = recordEvidence(tracker, 'vault-query', 'called');
|
|
223
|
+
tracker = advanceStep(tracker);
|
|
224
|
+
|
|
225
|
+
const filePath = persistTracker(tracker);
|
|
226
|
+
|
|
227
|
+
expect(existsSync(filePath)).toBe(true);
|
|
228
|
+
|
|
229
|
+
const loaded = loadTracker(tracker.runId);
|
|
230
|
+
expect(loaded).not.toBeNull();
|
|
231
|
+
expect(loaded!.skillName).toBe('persist-test');
|
|
232
|
+
expect(loaded!.currentStep).toBe(1);
|
|
233
|
+
expect(loaded!.evidence['vault-query'].value).toBe('called');
|
|
234
|
+
expect(loaded!.steps).toEqual(SAMPLE_STEPS);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('returns null for non-existent runId', () => {
|
|
238
|
+
const result = loadTracker('does-not-exist-12345');
|
|
239
|
+
expect(result).toBeNull();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('creates skill-runs directory if missing', () => {
|
|
243
|
+
const runsDir = join(tmpDir, '.soleri', 'skill-runs');
|
|
244
|
+
expect(existsSync(runsDir)).toBe(false);
|
|
245
|
+
|
|
246
|
+
const tracker = createTracker('dir-test', SAMPLE_STEPS);
|
|
247
|
+
persistTracker(tracker);
|
|
248
|
+
|
|
249
|
+
expect(existsSync(runsDir)).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('returns null for corrupted JSON', () => {
|
|
253
|
+
const runsDir = join(tmpDir, '.soleri', 'skill-runs');
|
|
254
|
+
mkdirSync(runsDir, { recursive: true });
|
|
255
|
+
writeFileSync(join(runsDir, 'bad-run.json'), '{ invalid json', 'utf-8');
|
|
256
|
+
|
|
257
|
+
const result = loadTracker('bad-run');
|
|
258
|
+
expect(result).toBeNull();
|
|
259
|
+
});
|
|
260
|
+
});
|