opencastle 0.31.7 → 0.32.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/README.md +4 -1
- package/bin/cli.mjs +15 -0
- package/dist/cli/agents.d.ts.map +1 -1
- package/dist/cli/agents.js +19 -5
- package/dist/cli/agents.js.map +1 -1
- package/dist/cli/artifacts-cli.d.ts +3 -0
- package/dist/cli/artifacts-cli.d.ts.map +1 -0
- package/dist/cli/artifacts-cli.js +36 -0
- package/dist/cli/artifacts-cli.js.map +1 -0
- package/dist/cli/baselines.d.ts.map +1 -1
- package/dist/cli/baselines.js +11 -0
- package/dist/cli/baselines.js.map +1 -1
- package/dist/cli/convoy/artifacts.d.ts +25 -0
- package/dist/cli/convoy/artifacts.d.ts.map +1 -0
- package/dist/cli/convoy/artifacts.js +129 -0
- package/dist/cli/convoy/artifacts.js.map +1 -0
- package/dist/cli/convoy/artifacts.test.d.ts +2 -0
- package/dist/cli/convoy/artifacts.test.d.ts.map +1 -0
- package/dist/cli/convoy/artifacts.test.js +169 -0
- package/dist/cli/convoy/artifacts.test.js.map +1 -0
- package/dist/cli/convoy/compaction.d.ts +23 -0
- package/dist/cli/convoy/compaction.d.ts.map +1 -0
- package/dist/cli/convoy/compaction.js +117 -0
- package/dist/cli/convoy/compaction.js.map +1 -0
- package/dist/cli/convoy/compaction.test.d.ts +2 -0
- package/dist/cli/convoy/compaction.test.d.ts.map +1 -0
- package/dist/cli/convoy/compaction.test.js +205 -0
- package/dist/cli/convoy/compaction.test.js.map +1 -0
- package/dist/cli/convoy/contracts.d.ts +22 -0
- package/dist/cli/convoy/contracts.d.ts.map +1 -0
- package/dist/cli/convoy/contracts.js +254 -0
- package/dist/cli/convoy/contracts.js.map +1 -0
- package/dist/cli/convoy/contracts.test.d.ts +2 -0
- package/dist/cli/convoy/contracts.test.d.ts.map +1 -0
- package/dist/cli/convoy/contracts.test.js +239 -0
- package/dist/cli/convoy/contracts.test.js.map +1 -0
- package/dist/cli/convoy/dag-analysis.d.ts +40 -0
- package/dist/cli/convoy/dag-analysis.d.ts.map +1 -0
- package/dist/cli/convoy/dag-analysis.js +282 -0
- package/dist/cli/convoy/dag-analysis.js.map +1 -0
- package/dist/cli/convoy/dag-analysis.test.d.ts +2 -0
- package/dist/cli/convoy/dag-analysis.test.d.ts.map +1 -0
- package/dist/cli/convoy/dag-analysis.test.js +289 -0
- package/dist/cli/convoy/dag-analysis.test.js.map +1 -0
- package/dist/cli/convoy/effort-scaling.d.ts +20 -0
- package/dist/cli/convoy/effort-scaling.d.ts.map +1 -0
- package/dist/cli/convoy/effort-scaling.js +82 -0
- package/dist/cli/convoy/effort-scaling.js.map +1 -0
- package/dist/cli/convoy/effort-scaling.test.d.ts +2 -0
- package/dist/cli/convoy/effort-scaling.test.d.ts.map +1 -0
- package/dist/cli/convoy/effort-scaling.test.js +120 -0
- package/dist/cli/convoy/effort-scaling.test.js.map +1 -0
- package/dist/cli/convoy/engine.d.ts.map +1 -1
- package/dist/cli/convoy/engine.js +280 -6
- package/dist/cli/convoy/engine.js.map +1 -1
- package/dist/cli/convoy/engine.test.js +155 -18
- package/dist/cli/convoy/engine.test.js.map +1 -1
- package/dist/cli/convoy/event-schemas.d.ts.map +1 -1
- package/dist/cli/convoy/event-schemas.js +55 -0
- package/dist/cli/convoy/event-schemas.js.map +1 -1
- package/dist/cli/convoy/isolation.d.ts +27 -0
- package/dist/cli/convoy/isolation.d.ts.map +1 -0
- package/dist/cli/convoy/isolation.js +120 -0
- package/dist/cli/convoy/isolation.js.map +1 -0
- package/dist/cli/convoy/isolation.test.d.ts +2 -0
- package/dist/cli/convoy/isolation.test.d.ts.map +1 -0
- package/dist/cli/convoy/isolation.test.js +105 -0
- package/dist/cli/convoy/isolation.test.js.map +1 -0
- package/dist/cli/convoy/review-stages.d.ts +9 -0
- package/dist/cli/convoy/review-stages.d.ts.map +1 -0
- package/dist/cli/convoy/review-stages.js +134 -0
- package/dist/cli/convoy/review-stages.js.map +1 -0
- package/dist/cli/convoy/review-stages.test.d.ts +2 -0
- package/dist/cli/convoy/review-stages.test.d.ts.map +1 -0
- package/dist/cli/convoy/review-stages.test.js +197 -0
- package/dist/cli/convoy/review-stages.test.js.map +1 -0
- package/dist/cli/convoy/skill-refinement.d.ts +39 -0
- package/dist/cli/convoy/skill-refinement.d.ts.map +1 -0
- package/dist/cli/convoy/skill-refinement.js +239 -0
- package/dist/cli/convoy/skill-refinement.js.map +1 -0
- package/dist/cli/convoy/skill-refinement.test.d.ts +2 -0
- package/dist/cli/convoy/skill-refinement.test.d.ts.map +1 -0
- package/dist/cli/convoy/skill-refinement.test.js +230 -0
- package/dist/cli/convoy/skill-refinement.test.js.map +1 -0
- package/dist/cli/convoy/spec-builder.d.ts +1 -0
- package/dist/cli/convoy/spec-builder.d.ts.map +1 -1
- package/dist/cli/convoy/spec-builder.js +11 -0
- package/dist/cli/convoy/spec-builder.js.map +1 -1
- package/dist/cli/convoy/spec-builder.test.js +54 -0
- package/dist/cli/convoy/spec-builder.test.js.map +1 -1
- package/dist/cli/convoy/store.d.ts +3 -2
- package/dist/cli/convoy/store.d.ts.map +1 -1
- package/dist/cli/convoy/store.js +20 -2
- package/dist/cli/convoy/store.js.map +1 -1
- package/dist/cli/convoy/store.test.js +15 -15
- package/dist/cli/convoy/store.test.js.map +1 -1
- package/dist/cli/convoy/tdd-gate.d.ts +15 -0
- package/dist/cli/convoy/tdd-gate.d.ts.map +1 -0
- package/dist/cli/convoy/tdd-gate.js +119 -0
- package/dist/cli/convoy/tdd-gate.js.map +1 -0
- package/dist/cli/convoy/tdd-gate.test.d.ts +2 -0
- package/dist/cli/convoy/tdd-gate.test.d.ts.map +1 -0
- package/dist/cli/convoy/tdd-gate.test.js +227 -0
- package/dist/cli/convoy/tdd-gate.test.js.map +1 -0
- package/dist/cli/convoy/types.d.ts +91 -0
- package/dist/cli/convoy/types.d.ts.map +1 -1
- package/dist/cli/convoy/types.js +8 -0
- package/dist/cli/convoy/types.js.map +1 -1
- package/dist/cli/insights.d.ts +3 -0
- package/dist/cli/insights.d.ts.map +1 -0
- package/dist/cli/insights.js +94 -0
- package/dist/cli/insights.js.map +1 -0
- package/dist/cli/lesson.d.ts.map +1 -1
- package/dist/cli/lesson.js +7 -0
- package/dist/cli/lesson.js.map +1 -1
- package/dist/cli/log.d.ts.map +1 -1
- package/dist/cli/log.js +7 -0
- package/dist/cli/log.js.map +1 -1
- package/dist/cli/package-config.d.ts +12 -0
- package/dist/cli/package-config.d.ts.map +1 -0
- package/dist/cli/package-config.js +37 -0
- package/dist/cli/package-config.js.map +1 -0
- package/dist/cli/package.d.ts +23 -0
- package/dist/cli/package.d.ts.map +1 -0
- package/dist/cli/package.js +285 -0
- package/dist/cli/package.js.map +1 -0
- package/dist/cli/package.test.d.ts +2 -0
- package/dist/cli/package.test.d.ts.map +1 -0
- package/dist/cli/package.test.js +236 -0
- package/dist/cli/package.test.js.map +1 -0
- package/dist/cli/pipeline.d.ts +6 -0
- package/dist/cli/pipeline.d.ts.map +1 -1
- package/dist/cli/pipeline.js +15 -2
- package/dist/cli/pipeline.js.map +1 -1
- package/dist/cli/run/schema.d.ts.map +1 -1
- package/dist/cli/run/schema.js +32 -0
- package/dist/cli/run/schema.js.map +1 -1
- package/dist/cli/run/schema.test.js +51 -0
- package/dist/cli/run/schema.test.js.map +1 -1
- package/dist/cli/skills.d.ts +3 -0
- package/dist/cli/skills.d.ts.map +1 -0
- package/dist/cli/skills.js +107 -0
- package/dist/cli/skills.js.map +1 -0
- package/dist/cli/types.d.ts +4 -1
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/dashboard/scripts/etl.d.ts.map +1 -1
- package/dist/dashboard/scripts/etl.js +44 -11
- package/dist/dashboard/scripts/etl.js.map +1 -1
- package/package.json +2 -1
- package/src/cli/agents.ts +20 -5
- package/src/cli/artifacts-cli.ts +41 -0
- package/src/cli/baselines.ts +12 -0
- package/src/cli/convoy/artifacts.test.ts +201 -0
- package/src/cli/convoy/artifacts.ts +186 -0
- package/src/cli/convoy/compaction.test.ts +245 -0
- package/src/cli/convoy/compaction.ts +164 -0
- package/src/cli/convoy/contracts.test.ts +279 -0
- package/src/cli/convoy/contracts.ts +280 -0
- package/src/cli/convoy/dag-analysis.test.ts +349 -0
- package/src/cli/convoy/dag-analysis.ts +371 -0
- package/src/cli/convoy/effort-scaling.test.ts +140 -0
- package/src/cli/convoy/effort-scaling.ts +90 -0
- package/src/cli/convoy/engine.test.ts +175 -18
- package/src/cli/convoy/engine.ts +301 -7
- package/src/cli/convoy/event-schemas.ts +55 -0
- package/src/cli/convoy/isolation.test.ts +137 -0
- package/src/cli/convoy/isolation.ts +165 -0
- package/src/cli/convoy/review-stages.test.ts +235 -0
- package/src/cli/convoy/review-stages.ts +166 -0
- package/src/cli/convoy/skill-refinement.test.ts +277 -0
- package/src/cli/convoy/skill-refinement.ts +306 -0
- package/src/cli/convoy/spec-builder.test.ts +61 -0
- package/src/cli/convoy/spec-builder.ts +9 -0
- package/src/cli/convoy/store.test.ts +15 -15
- package/src/cli/convoy/store.ts +26 -4
- package/src/cli/convoy/tdd-gate.test.ts +281 -0
- package/src/cli/convoy/tdd-gate.ts +154 -0
- package/src/cli/convoy/types.ts +51 -0
- package/src/cli/insights.ts +99 -0
- package/src/cli/lesson.ts +8 -0
- package/src/cli/log.ts +8 -0
- package/src/cli/package-config.ts +48 -0
- package/src/cli/package.test.ts +276 -0
- package/src/cli/package.ts +329 -0
- package/src/cli/pipeline.ts +21 -2
- package/src/cli/run/schema.test.ts +58 -0
- package/src/cli/run/schema.ts +33 -0
- package/src/cli/skills.ts +121 -0
- package/src/cli/types.ts +4 -1
- package/src/dashboard/dist/_astro/index.D6quLrA6.css +1 -0
- package/src/dashboard/dist/data/convoy-list.json +21 -7
- package/src/dashboard/dist/data/convoys/demo-api-v2.json +3 -3
- package/src/dashboard/dist/data/convoys/demo-auth-revamp.json +5 -5
- package/src/dashboard/dist/data/convoys/demo-convoy-1.json +2 -2
- package/src/dashboard/dist/data/convoys/demo-convoy-2.json +1 -1
- package/src/dashboard/dist/data/convoys/demo-dashboard-ui.json +7 -7
- package/src/dashboard/dist/data/convoys/demo-data-pipeline.json +3 -3
- package/src/dashboard/dist/data/convoys/demo-deploy-ci.json +2 -2
- package/src/dashboard/dist/data/convoys/demo-docs-update.json +2 -2
- package/src/dashboard/dist/data/convoys/demo-perf-opt.json +4 -4
- package/src/dashboard/dist/index.html +306 -33
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/dashboard/public/data/convoy-list.json +21 -7
- package/src/dashboard/public/data/convoys/demo-api-v2.json +3 -3
- package/src/dashboard/public/data/convoys/demo-auth-revamp.json +5 -5
- package/src/dashboard/public/data/convoys/demo-convoy-1.json +2 -2
- package/src/dashboard/public/data/convoys/demo-convoy-2.json +1 -1
- package/src/dashboard/public/data/convoys/demo-dashboard-ui.json +7 -7
- package/src/dashboard/public/data/convoys/demo-data-pipeline.json +3 -3
- package/src/dashboard/public/data/convoys/demo-deploy-ci.json +2 -2
- package/src/dashboard/public/data/convoys/demo-docs-update.json +2 -2
- package/src/dashboard/public/data/convoys/demo-perf-opt.json +4 -4
- package/src/dashboard/scripts/etl.test.ts +14 -0
- package/src/dashboard/scripts/etl.ts +48 -16
- package/src/dashboard/scripts/generate-demo-db.ts +18 -10
- package/src/dashboard/src/pages/index.astro +348 -45
- package/src/dashboard/src/styles/dashboard.css +56 -0
- package/src/orchestrator/prompts/assess-complexity.prompt.md +13 -0
- package/src/orchestrator/prompts/generate-convoy.prompt.md +19 -0
- package/src/dashboard/dist/_astro/index.BRDFmNzR.css +0 -1
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { mkdtempSync, rmSync, existsSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import {
|
|
6
|
+
getArtifactDir,
|
|
7
|
+
writeArtifact,
|
|
8
|
+
listArtifacts,
|
|
9
|
+
readArtifact,
|
|
10
|
+
extractArtifactRefs,
|
|
11
|
+
pruneArtifacts,
|
|
12
|
+
} from './artifacts.js'
|
|
13
|
+
|
|
14
|
+
describe('artifacts', () => {
|
|
15
|
+
let tmpDir: string
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'artifacts-test-'))
|
|
19
|
+
vi.spyOn(process, 'cwd').mockReturnValue(tmpDir)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.restoreAllMocks()
|
|
24
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('writeArtifact', () => {
|
|
28
|
+
it('creates file in correct directory structure', () => {
|
|
29
|
+
const artifact = writeArtifact('convoy-1', 'task-1', 'report.md', '# Report\nContent here', 'report')
|
|
30
|
+
expect(artifact.path).toContain('convoy-1')
|
|
31
|
+
expect(artifact.path).toContain('task-1')
|
|
32
|
+
expect(artifact.path).toContain('report.md')
|
|
33
|
+
expect(existsSync(artifact.path)).toBe(true)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('creates .meta.json sidecar', () => {
|
|
37
|
+
const artifact = writeArtifact('convoy-1', 'task-1', 'report.md', '# Report\nContent here', 'report')
|
|
38
|
+
const metaPath = artifact.path + '.meta.json'
|
|
39
|
+
expect(existsSync(metaPath)).toBe(true)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('returns Artifact with correct metadata', () => {
|
|
43
|
+
const content = '# Report\nLine 2'
|
|
44
|
+
const artifact = writeArtifact('convoy-1', 'task-1', 'report.md', content, 'report')
|
|
45
|
+
expect(artifact.task_id).toBe('task-1')
|
|
46
|
+
expect(artifact.convoy_id).toBe('convoy-1')
|
|
47
|
+
expect(artifact.filename).toBe('report.md')
|
|
48
|
+
expect(artifact.type).toBe('report')
|
|
49
|
+
expect(artifact.size_bytes).toBe(Buffer.byteLength(content, 'utf8'))
|
|
50
|
+
expect(artifact.summary).toBe('# Report')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('summary is truncated to 120 chars when first line is long', () => {
|
|
54
|
+
const longFirstLine = 'A'.repeat(200)
|
|
55
|
+
const content = longFirstLine + '\nSecond line'
|
|
56
|
+
const artifact = writeArtifact('convoy-1', 'task-1', 'long.md', content, 'report')
|
|
57
|
+
expect(artifact.summary.length).toBe(120)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('listArtifacts', () => {
|
|
62
|
+
it('returns refs for all files in task dir excluding .meta.json', () => {
|
|
63
|
+
writeArtifact('convoy-1', 'task-1', 'report.md', '# Report', 'report')
|
|
64
|
+
writeArtifact('convoy-1', 'task-1', 'migration.sql', 'ALTER TABLE users', 'code')
|
|
65
|
+
const refs = listArtifacts('convoy-1', 'task-1')
|
|
66
|
+
expect(refs).toHaveLength(2)
|
|
67
|
+
const filenames = refs.map(r => r.filename)
|
|
68
|
+
expect(filenames).toContain('report.md')
|
|
69
|
+
expect(filenames).toContain('migration.sql')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('returns empty array when dir does not exist', () => {
|
|
73
|
+
const refs = listArtifacts('nonexistent-convoy', 'nonexistent-task')
|
|
74
|
+
expect(refs).toHaveLength(0)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('populates summary from .meta.json sidecar', () => {
|
|
78
|
+
writeArtifact('convoy-1', 'task-1', 'diff.patch', 'diff --git a/file.ts\nchanges here', 'diff')
|
|
79
|
+
const refs = listArtifacts('convoy-1', 'task-1')
|
|
80
|
+
expect(refs[0].summary).toBe('diff --git a/file.ts')
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe('readArtifact', () => {
|
|
85
|
+
it('returns file content', () => {
|
|
86
|
+
const content = '# Report\nDetailed content here'
|
|
87
|
+
const artifact = writeArtifact('convoy-1', 'task-1', 'report.md', content, 'report')
|
|
88
|
+
const ref = { task_id: 'task-1', filename: 'report.md', summary: '# Report', path: artifact.path }
|
|
89
|
+
expect(readArtifact(ref)).toBe(content)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('extractArtifactRefs', () => {
|
|
94
|
+
it('parses [ARTIFACT: filename] summary pattern correctly', () => {
|
|
95
|
+
writeArtifact('convoy-1', 'task-1', 'report.md', '# Report', 'report')
|
|
96
|
+
const output = 'I completed the work.\n[ARTIFACT: report.md] Full analysis report\nSee the artifact for details.'
|
|
97
|
+
const refs = extractArtifactRefs('task-1', 'convoy-1', output)
|
|
98
|
+
expect(refs).toHaveLength(1)
|
|
99
|
+
expect(refs[0].filename).toBe('report.md')
|
|
100
|
+
expect(refs[0].summary).toBe('Full analysis report')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('parses multiple artifacts in one output', () => {
|
|
104
|
+
writeArtifact('convoy-1', 'task-1', 'report.md', '# Report', 'report')
|
|
105
|
+
writeArtifact('convoy-1', 'task-1', 'migration.sql', 'ALTER TABLE', 'code')
|
|
106
|
+
const output = '[ARTIFACT: report.md] Analysis report\n[ARTIFACT: migration.sql] Database migration script'
|
|
107
|
+
const refs = extractArtifactRefs('task-1', 'convoy-1', output)
|
|
108
|
+
expect(refs).toHaveLength(2)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('logs warning to stderr for referenced but missing artifacts', () => {
|
|
112
|
+
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true)
|
|
113
|
+
const output = '[ARTIFACT: missing.md] A report that does not exist on disk'
|
|
114
|
+
const refs = extractArtifactRefs('task-1', 'convoy-1', output)
|
|
115
|
+
expect(refs).toHaveLength(0)
|
|
116
|
+
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('[artifacts] Warning'))
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('returns empty array when no artifact patterns found', () => {
|
|
120
|
+
const refs = extractArtifactRefs('task-1', 'convoy-1', 'No artifacts here, just a normal response.')
|
|
121
|
+
expect(refs).toHaveLength(0)
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe('pruneArtifacts', () => {
|
|
126
|
+
it('removes convoy dirs beyond keepCount', () => {
|
|
127
|
+
writeArtifact('convoy-a', 'task-1', 'file.md', 'content', 'report')
|
|
128
|
+
writeArtifact('convoy-b', 'task-1', 'file.md', 'content', 'report')
|
|
129
|
+
writeArtifact('convoy-c', 'task-1', 'file.md', 'content', 'report')
|
|
130
|
+
const result = pruneArtifacts(1)
|
|
131
|
+
expect(result.removed).toBe(2)
|
|
132
|
+
expect(result.freed_bytes).toBeGreaterThan(0)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('returns zero removed when within keepCount', () => {
|
|
136
|
+
writeArtifact('convoy-1', 'task-1', 'file.md', 'content', 'report')
|
|
137
|
+
writeArtifact('convoy-2', 'task-1', 'file.md', 'content', 'report')
|
|
138
|
+
const result = pruneArtifacts(5)
|
|
139
|
+
expect(result.removed).toBe(0)
|
|
140
|
+
expect(result.freed_bytes).toBe(0)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('returns zero when no artifacts directory exists', () => {
|
|
144
|
+
const result = pruneArtifacts(10)
|
|
145
|
+
expect(result.removed).toBe(0)
|
|
146
|
+
expect(result.freed_bytes).toBe(0)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('returns correct freed bytes count', () => {
|
|
150
|
+
const content = 'A'.repeat(1000)
|
|
151
|
+
writeArtifact('convoy-old1', 'task-1', 'large.md', content, 'report')
|
|
152
|
+
writeArtifact('convoy-old2', 'task-1', 'large.md', content, 'report')
|
|
153
|
+
writeArtifact('convoy-new', 'task-1', 'large.md', content, 'report')
|
|
154
|
+
const result = pruneArtifacts(1)
|
|
155
|
+
expect(result.removed).toBe(2)
|
|
156
|
+
expect(result.freed_bytes).toBeGreaterThan(0)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe('round-trip', () => {
|
|
161
|
+
it('write → list → read produces identical content', () => {
|
|
162
|
+
const content = '# Report\nLine 2\nLine 3'
|
|
163
|
+
writeArtifact('convoy-1', 'task-1', 'report.md', content, 'report')
|
|
164
|
+
const refs = listArtifacts('convoy-1', 'task-1')
|
|
165
|
+
expect(refs).toHaveLength(1)
|
|
166
|
+
const retrieved = readArtifact(refs[0])
|
|
167
|
+
expect(retrieved).toBe(content)
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
describe('path sanitization', () => {
|
|
172
|
+
it('rejects filenames with ..', () => {
|
|
173
|
+
expect(() => writeArtifact('convoy-1', 'task-1', '../evil.ts', 'content', 'code')).toThrow()
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('rejects filenames with /', () => {
|
|
177
|
+
expect(() => writeArtifact('convoy-1', 'task-1', 'sub/evil.ts', 'content', 'code')).toThrow()
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('rejects convoy IDs with path traversal', () => {
|
|
181
|
+
expect(() => writeArtifact('../evil', 'task-1', 'file.md', 'content', 'report')).toThrow()
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('rejects task IDs with backslash', () => {
|
|
185
|
+
expect(() => writeArtifact('convoy-1', 'task\\evil', 'file.md', 'content', 'report')).toThrow()
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
describe('getArtifactDir', () => {
|
|
190
|
+
it('returns path ending with trailing slash', () => {
|
|
191
|
+
const dir = getArtifactDir('convoy-1', 'task-1')
|
|
192
|
+
expect(dir.endsWith('/')).toBe(true)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('includes convoy-id and task-id in path', () => {
|
|
196
|
+
const dir = getArtifactDir('convoy-abc', 'task-xyz')
|
|
197
|
+
expect(dir).toContain('convoy-abc')
|
|
198
|
+
expect(dir).toContain('task-xyz')
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
})
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
statSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from 'node:fs'
|
|
10
|
+
import { basename, join } from 'node:path'
|
|
11
|
+
|
|
12
|
+
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export interface Artifact {
|
|
15
|
+
task_id: string
|
|
16
|
+
convoy_id: string
|
|
17
|
+
filename: string
|
|
18
|
+
type: 'report' | 'code' | 'data' | 'diff' | 'log' | 'other'
|
|
19
|
+
size_bytes: number
|
|
20
|
+
summary: string
|
|
21
|
+
path: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ArtifactRef {
|
|
25
|
+
task_id: string
|
|
26
|
+
filename: string
|
|
27
|
+
summary: string
|
|
28
|
+
path: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ArtifactMeta {
|
|
32
|
+
type: Artifact['type']
|
|
33
|
+
summary: string
|
|
34
|
+
size_bytes: number
|
|
35
|
+
created_at: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Sanitization ──────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
function sanitizeSegment(input: string): string {
|
|
41
|
+
if (input.includes('..') || input.includes('/') || input.includes('\\')) {
|
|
42
|
+
throw new Error(`Invalid path segment "${input}": path traversal characters not allowed`)
|
|
43
|
+
}
|
|
44
|
+
return input.replace(/[^a-zA-Z0-9\-_.]/g, '')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Core ──────────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
export function getArtifactDir(convoyId: string, taskId: string): string {
|
|
50
|
+
const safeConvoyId = sanitizeSegment(convoyId)
|
|
51
|
+
const safeTaskId = sanitizeSegment(taskId)
|
|
52
|
+
return join(process.cwd(), '.opencastle', 'artifacts', safeConvoyId, safeTaskId) + '/'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function writeArtifact(
|
|
56
|
+
convoyId: string,
|
|
57
|
+
taskId: string,
|
|
58
|
+
filename: string,
|
|
59
|
+
content: string,
|
|
60
|
+
type: Artifact['type'],
|
|
61
|
+
): Artifact {
|
|
62
|
+
const safeFilename = sanitizeSegment(filename)
|
|
63
|
+
const dir = getArtifactDir(convoyId, taskId)
|
|
64
|
+
mkdirSync(dir, { recursive: true })
|
|
65
|
+
|
|
66
|
+
const filePath = join(dir, safeFilename)
|
|
67
|
+
writeFileSync(filePath, content, 'utf8')
|
|
68
|
+
|
|
69
|
+
const size_bytes = Buffer.byteLength(content, 'utf8')
|
|
70
|
+
const firstLine = content.split('\n')[0] ?? ''
|
|
71
|
+
const summary = firstLine.slice(0, 120)
|
|
72
|
+
|
|
73
|
+
const meta: ArtifactMeta = { type, summary, size_bytes, created_at: new Date().toISOString() }
|
|
74
|
+
writeFileSync(join(dir, safeFilename + '.meta.json'), JSON.stringify(meta, null, 2), 'utf8')
|
|
75
|
+
|
|
76
|
+
return { task_id: taskId, convoy_id: convoyId, filename: safeFilename, type, size_bytes, summary, path: filePath }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function listArtifacts(convoyId: string, taskId: string): ArtifactRef[] {
|
|
80
|
+
const dir = getArtifactDir(convoyId, taskId)
|
|
81
|
+
if (!existsSync(dir)) return []
|
|
82
|
+
|
|
83
|
+
const refs: ArtifactRef[] = []
|
|
84
|
+
for (const entry of readdirSync(dir)) {
|
|
85
|
+
if (entry.endsWith('.meta.json')) continue
|
|
86
|
+
|
|
87
|
+
const filePath = join(dir, entry)
|
|
88
|
+
const metaPath = join(dir, entry + '.meta.json')
|
|
89
|
+
|
|
90
|
+
let summary = ''
|
|
91
|
+
if (existsSync(metaPath)) {
|
|
92
|
+
try {
|
|
93
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf8')) as ArtifactMeta
|
|
94
|
+
summary = meta.summary
|
|
95
|
+
} catch { /* fallback */ }
|
|
96
|
+
}
|
|
97
|
+
if (!summary) {
|
|
98
|
+
try {
|
|
99
|
+
const firstLine = readFileSync(filePath, 'utf8').split('\n')[0] ?? ''
|
|
100
|
+
summary = firstLine.slice(0, 120)
|
|
101
|
+
} catch { /* non-critical */ }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
refs.push({ task_id: taskId, filename: entry, summary, path: filePath })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return refs
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function readArtifact(ref: ArtifactRef): string {
|
|
111
|
+
return readFileSync(ref.path, 'utf8')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function extractArtifactRefs(taskId: string, convoyId: string, output: string): ArtifactRef[] {
|
|
115
|
+
const pattern = /\[ARTIFACT:\s*([^\]]+)\]\s*(.+)/g
|
|
116
|
+
const refs: ArtifactRef[] = []
|
|
117
|
+
let match: RegExpExecArray | null
|
|
118
|
+
|
|
119
|
+
while ((match = pattern.exec(output)) !== null) {
|
|
120
|
+
// Use basename to prevent path traversal from untrusted agent output
|
|
121
|
+
const filename = basename(match[1].trim())
|
|
122
|
+
const summary = match[2].trim()
|
|
123
|
+
|
|
124
|
+
if (!filename || filename === '..') {
|
|
125
|
+
process.stderr.write('[artifacts] Warning: invalid artifact filename from agent output\n')
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const dir = getArtifactDir(convoyId, taskId)
|
|
130
|
+
const filePath = join(dir, filename)
|
|
131
|
+
|
|
132
|
+
if (!existsSync(filePath)) {
|
|
133
|
+
process.stderr.write(`[artifacts] Warning: referenced artifact not found: ${filePath}\n`)
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
refs.push({ task_id: taskId, filename, summary, path: filePath })
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return refs
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function pruneArtifacts(keepCount: number): { removed: number; freed_bytes: number } {
|
|
144
|
+
const artifactsRoot = join(process.cwd(), '.opencastle', 'artifacts')
|
|
145
|
+
if (!existsSync(artifactsRoot)) return { removed: 0, freed_bytes: 0 }
|
|
146
|
+
|
|
147
|
+
const convoyDirs = readdirSync(artifactsRoot)
|
|
148
|
+
.map(name => {
|
|
149
|
+
const dirPath = join(artifactsRoot, name)
|
|
150
|
+
try {
|
|
151
|
+
return { name, path: dirPath, mtime: statSync(dirPath).mtime.getTime() }
|
|
152
|
+
} catch {
|
|
153
|
+
return { name, path: dirPath, mtime: 0 }
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
.sort((a, b) => b.mtime - a.mtime)
|
|
157
|
+
|
|
158
|
+
const toRemove = convoyDirs.slice(keepCount)
|
|
159
|
+
let removed = 0
|
|
160
|
+
let freed_bytes = 0
|
|
161
|
+
|
|
162
|
+
for (const dir of toRemove) {
|
|
163
|
+
try {
|
|
164
|
+
freed_bytes += calcDirSize(dir.path)
|
|
165
|
+
rmSync(dir.path, { recursive: true, force: true })
|
|
166
|
+
removed++
|
|
167
|
+
} catch { /* non-critical */ }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { removed, freed_bytes }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function calcDirSize(dirPath: string): number {
|
|
174
|
+
let total = 0
|
|
175
|
+
try {
|
|
176
|
+
for (const entry of readdirSync(dirPath)) {
|
|
177
|
+
const p = join(dirPath, entry)
|
|
178
|
+
try {
|
|
179
|
+
const s = statSync(p)
|
|
180
|
+
if (s.isDirectory()) total += calcDirSize(p)
|
|
181
|
+
else total += s.size
|
|
182
|
+
} catch { /* skip */ }
|
|
183
|
+
}
|
|
184
|
+
} catch { /* skip */ }
|
|
185
|
+
return total
|
|
186
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest'
|
|
2
|
+
import { mkdtempSync, rmSync, existsSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import {
|
|
6
|
+
shouldCompact,
|
|
7
|
+
generateCompactionPrompt,
|
|
8
|
+
parseCompactionSummary,
|
|
9
|
+
saveCompaction,
|
|
10
|
+
loadCompaction,
|
|
11
|
+
buildContinuationPrompt,
|
|
12
|
+
canCompact,
|
|
13
|
+
getMaxCompactions,
|
|
14
|
+
getCompactionDir,
|
|
15
|
+
MODEL_CONTEXT_WINDOWS,
|
|
16
|
+
type CompactionSummary,
|
|
17
|
+
} from './compaction.js'
|
|
18
|
+
import type { CompactionConfig } from './types.js'
|
|
19
|
+
|
|
20
|
+
const baseConfig: CompactionConfig = {
|
|
21
|
+
enabled: true,
|
|
22
|
+
token_threshold_pct: 70,
|
|
23
|
+
summary_max_tokens: 4096,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('shouldCompact', () => {
|
|
27
|
+
it('returns false when config.enabled is false', () => {
|
|
28
|
+
expect(shouldCompact(150_000, 'claude-sonnet-4-6', { ...baseConfig, enabled: false })).toBe(false)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('returns false below threshold', () => {
|
|
32
|
+
// 69% of 200_000 = 138_000
|
|
33
|
+
expect(shouldCompact(138_000, 'claude-sonnet-4-6', baseConfig)).toBe(false)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('returns true at exactly the threshold', () => {
|
|
37
|
+
// 70% of 200_000 = 140_000
|
|
38
|
+
expect(shouldCompact(140_000, 'claude-sonnet-4-6', baseConfig)).toBe(true)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('returns true above threshold', () => {
|
|
42
|
+
expect(shouldCompact(180_000, 'claude-sonnet-4-6', baseConfig)).toBe(true)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('uses DEFAULT_CONTEXT_WINDOW (128_000) for unknown model', () => {
|
|
46
|
+
// 70% of 128_000 = 89_600
|
|
47
|
+
expect(shouldCompact(90_000, 'unknown-model-xyz', baseConfig)).toBe(true)
|
|
48
|
+
expect(shouldCompact(88_000, 'unknown-model-xyz', baseConfig)).toBe(false)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('uses correct window for gpt-5-mini (128_000)', () => {
|
|
52
|
+
expect(MODEL_CONTEXT_WINDOWS['gpt-5-mini']).toBe(128_000)
|
|
53
|
+
expect(shouldCompact(90_000, 'gpt-5-mini', baseConfig)).toBe(true)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('generateCompactionPrompt', () => {
|
|
58
|
+
it('returns a string containing COMPACTION_SUMMARY marker', () => {
|
|
59
|
+
const prompt = generateCompactionPrompt('task-42')
|
|
60
|
+
expect(prompt).toContain('COMPACTION_SUMMARY')
|
|
61
|
+
expect(prompt).toContain('Context Compaction Required')
|
|
62
|
+
expect(typeof prompt).toBe('string')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('includes JSON structure placeholders', () => {
|
|
66
|
+
const prompt = generateCompactionPrompt('task-1')
|
|
67
|
+
expect(prompt).toContain('"phase"')
|
|
68
|
+
expect(prompt).toContain('"completed_steps"')
|
|
69
|
+
expect(prompt).toContain('"pending_steps"')
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('parseCompactionSummary', () => {
|
|
74
|
+
it('parses valid summary from agent output', () => {
|
|
75
|
+
const output = [
|
|
76
|
+
'Some output text',
|
|
77
|
+
'',
|
|
78
|
+
'<!-- COMPACTION_SUMMARY',
|
|
79
|
+
'{',
|
|
80
|
+
' "phase": "implementation",',
|
|
81
|
+
' "completed_steps": ["created types", "wrote tests"],',
|
|
82
|
+
' "pending_steps": ["integrate with engine"],',
|
|
83
|
+
' "key_decisions": ["used valibot for validation"],',
|
|
84
|
+
' "files_modified": ["src/foo.ts"],',
|
|
85
|
+
' "artifact_refs": [".opencastle/artifacts/convoy-1/task-1/report.md"]',
|
|
86
|
+
'}',
|
|
87
|
+
'-->',
|
|
88
|
+
'',
|
|
89
|
+
'More text',
|
|
90
|
+
].join('\n')
|
|
91
|
+
const result = parseCompactionSummary(output, 'task-1', 'convoy-1')
|
|
92
|
+
expect(result).not.toBeNull()
|
|
93
|
+
expect(result!.task_id).toBe('task-1')
|
|
94
|
+
expect(result!.convoy_id).toBe('convoy-1')
|
|
95
|
+
expect(result!.phase).toBe('implementation')
|
|
96
|
+
expect(result!.completed_steps).toEqual(['created types', 'wrote tests'])
|
|
97
|
+
expect(result!.pending_steps).toEqual(['integrate with engine'])
|
|
98
|
+
expect(result!.key_decisions).toEqual(['used valibot for validation'])
|
|
99
|
+
expect(result!.files_modified).toEqual(['src/foo.ts'])
|
|
100
|
+
expect(result!.artifact_refs).toEqual(['.opencastle/artifacts/convoy-1/task-1/report.md'])
|
|
101
|
+
expect(typeof result!.timestamp).toBe('string')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('returns null when no COMPACTION_SUMMARY marker found', () => {
|
|
105
|
+
const result = parseCompactionSummary('just some output without the marker', 'task-1', 'convoy-1')
|
|
106
|
+
expect(result).toBeNull()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('returns null for invalid JSON inside the marker', () => {
|
|
110
|
+
const output = '<!-- COMPACTION_SUMMARY\nnot valid json\n-->'
|
|
111
|
+
const result = parseCompactionSummary(output, 'task-1', 'convoy-1')
|
|
112
|
+
expect(result).toBeNull()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('uses empty arrays for missing or non-array fields', () => {
|
|
116
|
+
const output = '<!-- COMPACTION_SUMMARY\n{"phase": "testing"}\n-->'
|
|
117
|
+
const result = parseCompactionSummary(output, 'task-1', 'convoy-1')
|
|
118
|
+
expect(result).not.toBeNull()
|
|
119
|
+
expect(result!.completed_steps).toEqual([])
|
|
120
|
+
expect(result!.pending_steps).toEqual([])
|
|
121
|
+
expect(result!.files_modified).toEqual([])
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe('saveCompaction / loadCompaction', () => {
|
|
126
|
+
let tmpDir: string
|
|
127
|
+
|
|
128
|
+
afterEach(() => {
|
|
129
|
+
if (tmpDir && existsSync(tmpDir)) {
|
|
130
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('saves and restores a compaction summary', () => {
|
|
135
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'compaction-test-'))
|
|
136
|
+
const summary: CompactionSummary = {
|
|
137
|
+
task_id: 'task-1',
|
|
138
|
+
convoy_id: 'convoy-abc',
|
|
139
|
+
phase: 'testing',
|
|
140
|
+
completed_steps: ['step A'],
|
|
141
|
+
pending_steps: ['step B'],
|
|
142
|
+
key_decisions: ['chose approach X'],
|
|
143
|
+
files_modified: ['src/foo.ts'],
|
|
144
|
+
artifact_refs: [],
|
|
145
|
+
timestamp: new Date().toISOString(),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const origCwd = process.cwd()
|
|
149
|
+
process.chdir(tmpDir)
|
|
150
|
+
try {
|
|
151
|
+
const savedPath = saveCompaction('convoy-abc', 'task-1', summary, 1)
|
|
152
|
+
expect(existsSync(savedPath)).toBe(true)
|
|
153
|
+
const loaded = loadCompaction(savedPath)
|
|
154
|
+
expect(loaded).not.toBeNull()
|
|
155
|
+
expect(loaded!.task_id).toBe('task-1')
|
|
156
|
+
expect(loaded!.phase).toBe('testing')
|
|
157
|
+
expect(loaded!.completed_steps).toEqual(['step A'])
|
|
158
|
+
} finally {
|
|
159
|
+
process.chdir(origCwd)
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('returns null for a non-existent path', () => {
|
|
164
|
+
const result = loadCompaction('/tmp/definitely-does-not-exist-abc123.json')
|
|
165
|
+
expect(result).toBeNull()
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
describe('buildContinuationPrompt', () => {
|
|
170
|
+
let tmpDir: string
|
|
171
|
+
|
|
172
|
+
afterEach(() => {
|
|
173
|
+
if (tmpDir && existsSync(tmpDir)) {
|
|
174
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('includes original prompt, summary, and isolation preamble', () => {
|
|
179
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'compaction-test-'))
|
|
180
|
+
const origCwd = process.cwd()
|
|
181
|
+
process.chdir(tmpDir)
|
|
182
|
+
try {
|
|
183
|
+
const summary: CompactionSummary = {
|
|
184
|
+
task_id: 'task-1',
|
|
185
|
+
convoy_id: 'convoy-abc',
|
|
186
|
+
phase: 'integration',
|
|
187
|
+
completed_steps: ['wrote types'],
|
|
188
|
+
pending_steps: ['update engine'],
|
|
189
|
+
key_decisions: ['decided to reuse store'],
|
|
190
|
+
files_modified: ['src/types.ts'],
|
|
191
|
+
artifact_refs: [],
|
|
192
|
+
timestamp: new Date().toISOString(),
|
|
193
|
+
}
|
|
194
|
+
const savedPath = saveCompaction('convoy-abc', 'task-1', summary, 1)
|
|
195
|
+
const result = buildContinuationPrompt('Do the remaining work', savedPath, '## Isolation preamble\n')
|
|
196
|
+
expect(result).toContain('## Isolation preamble')
|
|
197
|
+
expect(result).toContain('Do the remaining work')
|
|
198
|
+
expect(result).toContain('Continuation from Compacted Context')
|
|
199
|
+
expect(result).toContain('wrote types')
|
|
200
|
+
expect(result).toContain('update engine')
|
|
201
|
+
} finally {
|
|
202
|
+
process.chdir(origCwd)
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('handles missing summary file gracefully', () => {
|
|
207
|
+
const result = buildContinuationPrompt(
|
|
208
|
+
'Do the work',
|
|
209
|
+
'/tmp/missing-summary-def456.json',
|
|
210
|
+
'## Preamble\n',
|
|
211
|
+
)
|
|
212
|
+
expect(result).toContain('Do the work')
|
|
213
|
+
expect(result).toContain('## Preamble')
|
|
214
|
+
expect(result).not.toContain('Continuation from Compacted Context')
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
describe('canCompact', () => {
|
|
219
|
+
it('returns true below max (3)', () => {
|
|
220
|
+
expect(canCompact(0)).toBe(true)
|
|
221
|
+
expect(canCompact(1)).toBe(true)
|
|
222
|
+
expect(canCompact(2)).toBe(true)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('returns false at max (3)', () => {
|
|
226
|
+
expect(canCompact(3)).toBe(false)
|
|
227
|
+
expect(canCompact(4)).toBe(false)
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
describe('getMaxCompactions', () => {
|
|
232
|
+
it('returns 3', () => {
|
|
233
|
+
expect(getMaxCompactions()).toBe(3)
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
describe('getCompactionDir', () => {
|
|
238
|
+
it('returns path inside .opencastle/artifacts/{convoyId}/{taskId}', () => {
|
|
239
|
+
const result = getCompactionDir('convoy-abc', 'task-1')
|
|
240
|
+
expect(result).toContain('.opencastle')
|
|
241
|
+
expect(result).toContain('artifacts')
|
|
242
|
+
expect(result).toContain('convoy-abc')
|
|
243
|
+
expect(result).toContain('task-1')
|
|
244
|
+
})
|
|
245
|
+
})
|