edsger 0.45.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/dist/commands/workflow/executors/phase-executor.js +3 -1
  2. package/dist/commands/workflow/phase-orchestrator.js +1 -2
  3. package/dist/phases/app-store-generation/index.js +1 -2
  4. package/dist/phases/branch-planning/index.js +1 -2
  5. package/dist/phases/bug-fixing/analyzer.js +1 -2
  6. package/dist/phases/code-implementation/index.js +1 -2
  7. package/dist/phases/code-refine/index.js +1 -2
  8. package/dist/phases/code-review/index.js +1 -2
  9. package/dist/phases/code-testing/analyzer.js +1 -2
  10. package/dist/phases/feature-analysis/index.js +1 -2
  11. package/dist/phases/functional-testing/analyzer.js +1 -2
  12. package/dist/phases/growth-analysis/index.js +1 -2
  13. package/dist/phases/pr-execution/index.js +1 -0
  14. package/dist/phases/pr-splitting/index.js +1 -2
  15. package/dist/phases/run-sheet/index.js +7 -7
  16. package/dist/phases/run-sheet/render.js +3 -1
  17. package/dist/phases/smoke-test/agent.js +2 -4
  18. package/dist/phases/smoke-test/index.js +11 -6
  19. package/dist/phases/technical-design/index.js +1 -2
  20. package/dist/phases/test-cases-analysis/index.js +1 -2
  21. package/dist/phases/user-stories-analysis/index.js +1 -2
  22. package/package.json +3 -3
  23. package/tsconfig.build.json +4 -0
  24. package/tsconfig.json +3 -9
  25. package/dist/api/__tests__/app-store.test.d.ts +0 -7
  26. package/dist/api/__tests__/app-store.test.js +0 -60
  27. package/dist/api/__tests__/intelligence.test.d.ts +0 -11
  28. package/dist/api/__tests__/intelligence.test.js +0 -315
  29. package/dist/api/features/__tests__/feature-utils.test.d.ts +0 -4
  30. package/dist/api/features/__tests__/feature-utils.test.js +0 -370
  31. package/dist/api/features/__tests__/status-updater.test.d.ts +0 -4
  32. package/dist/api/features/__tests__/status-updater.test.js +0 -88
  33. package/dist/commands/build/__tests__/build.test.d.ts +0 -5
  34. package/dist/commands/build/__tests__/build.test.js +0 -206
  35. package/dist/commands/build/__tests__/detect-project.test.d.ts +0 -6
  36. package/dist/commands/build/__tests__/detect-project.test.js +0 -160
  37. package/dist/commands/build/__tests__/run-build.test.d.ts +0 -6
  38. package/dist/commands/build/__tests__/run-build.test.js +0 -433
  39. package/dist/commands/intelligence/__tests__/command.test.d.ts +0 -4
  40. package/dist/commands/intelligence/__tests__/command.test.js +0 -48
  41. package/dist/commands/workflow/core/__tests__/feature-filter.test.d.ts +0 -5
  42. package/dist/commands/workflow/core/__tests__/feature-filter.test.js +0 -316
  43. package/dist/commands/workflow/core/__tests__/pipeline-evaluator.test.d.ts +0 -4
  44. package/dist/commands/workflow/core/__tests__/pipeline-evaluator.test.js +0 -397
  45. package/dist/commands/workflow/core/__tests__/state-manager.test.d.ts +0 -4
  46. package/dist/commands/workflow/core/__tests__/state-manager.test.js +0 -384
  47. package/dist/config/__tests__/config.test.d.ts +0 -4
  48. package/dist/config/__tests__/config.test.js +0 -286
  49. package/dist/config/__tests__/feature-status.test.d.ts +0 -4
  50. package/dist/config/__tests__/feature-status.test.js +0 -111
  51. package/dist/errors/__tests__/index.test.d.ts +0 -4
  52. package/dist/errors/__tests__/index.test.js +0 -349
  53. package/dist/phases/app-store-generation/__tests__/agent.test.d.ts +0 -5
  54. package/dist/phases/app-store-generation/__tests__/agent.test.js +0 -142
  55. package/dist/phases/app-store-generation/__tests__/context.test.d.ts +0 -4
  56. package/dist/phases/app-store-generation/__tests__/context.test.js +0 -284
  57. package/dist/phases/app-store-generation/__tests__/prompts.test.d.ts +0 -4
  58. package/dist/phases/app-store-generation/__tests__/prompts.test.js +0 -122
  59. package/dist/phases/app-store-generation/__tests__/screenshot-composer.test.d.ts +0 -5
  60. package/dist/phases/app-store-generation/__tests__/screenshot-composer.test.js +0 -826
  61. package/dist/phases/code-review/__tests__/diff-utils.test.d.ts +0 -1
  62. package/dist/phases/code-review/__tests__/diff-utils.test.js +0 -101
  63. package/dist/phases/intelligence-analysis/__tests__/context.test.d.ts +0 -4
  64. package/dist/phases/intelligence-analysis/__tests__/context.test.js +0 -192
  65. package/dist/phases/intelligence-analysis/__tests__/matching.test.d.ts +0 -13
  66. package/dist/phases/intelligence-analysis/__tests__/matching.test.js +0 -154
  67. package/dist/phases/intelligence-analysis/__tests__/orchestration.test.d.ts +0 -5
  68. package/dist/phases/intelligence-analysis/__tests__/orchestration.test.js +0 -378
  69. package/dist/phases/intelligence-analysis/__tests__/prompts.test.d.ts +0 -4
  70. package/dist/phases/intelligence-analysis/__tests__/prompts.test.js +0 -33
  71. package/dist/phases/pr-execution/__tests__/file-assigner.test.d.ts +0 -1
  72. package/dist/phases/pr-execution/__tests__/file-assigner.test.js +0 -303
  73. package/dist/phases/pr-resolve/__tests__/checklist-learner.test.d.ts +0 -1
  74. package/dist/phases/pr-resolve/__tests__/checklist-learner.test.js +0 -157
  75. package/dist/phases/pr-resolve/__tests__/prompts.test.d.ts +0 -1
  76. package/dist/phases/pr-resolve/__tests__/prompts.test.js +0 -116
  77. package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.d.ts +0 -1
  78. package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.js +0 -138
  79. package/dist/phases/pr-resolve/__tests__/types.test.d.ts +0 -1
  80. package/dist/phases/pr-resolve/__tests__/types.test.js +0 -43
  81. package/dist/phases/pr-resolve/__tests__/workspace.test.d.ts +0 -1
  82. package/dist/phases/pr-resolve/__tests__/workspace.test.js +0 -111
  83. package/dist/phases/pr-review/__tests__/prompts.test.d.ts +0 -1
  84. package/dist/phases/pr-review/__tests__/prompts.test.js +0 -49
  85. package/dist/phases/pr-review/__tests__/review-comments.test.d.ts +0 -1
  86. package/dist/phases/pr-review/__tests__/review-comments.test.js +0 -110
  87. package/dist/phases/pr-shared/__tests__/agent-utils.test.d.ts +0 -1
  88. package/dist/phases/pr-shared/__tests__/agent-utils.test.js +0 -91
  89. package/dist/phases/pr-shared/__tests__/context.test.d.ts +0 -1
  90. package/dist/phases/pr-shared/__tests__/context.test.js +0 -94
  91. package/dist/phases/pr-splitting/__tests__/import-dep-validator.test.d.ts +0 -1
  92. package/dist/phases/pr-splitting/__tests__/import-dep-validator.test.js +0 -331
  93. package/dist/phases/release-sync/__tests__/github.test.d.ts +0 -9
  94. package/dist/phases/release-sync/__tests__/github.test.js +0 -123
  95. package/dist/phases/release-sync/__tests__/snapshot.test.d.ts +0 -8
  96. package/dist/phases/release-sync/__tests__/snapshot.test.js +0 -93
  97. package/dist/phases/smoke-test/__tests__/agent.test.d.ts +0 -4
  98. package/dist/phases/smoke-test/__tests__/agent.test.js +0 -85
  99. package/dist/services/coaching/__tests__/coaching-agent.test.d.ts +0 -1
  100. package/dist/services/coaching/__tests__/coaching-agent.test.js +0 -74
  101. package/dist/services/coaching/__tests__/coaching-loop.test.d.ts +0 -1
  102. package/dist/services/coaching/__tests__/coaching-loop.test.js +0 -59
  103. package/dist/services/coaching/__tests__/self-rating.test.d.ts +0 -1
  104. package/dist/services/coaching/__tests__/self-rating.test.js +0 -188
  105. package/dist/services/phase-hooks/__tests__/bindings-fetcher.test.d.ts +0 -1
  106. package/dist/services/phase-hooks/__tests__/bindings-fetcher.test.js +0 -122
  107. package/dist/services/phase-hooks/__tests__/hook-executor.test.d.ts +0 -1
  108. package/dist/services/phase-hooks/__tests__/hook-executor.test.js +0 -321
  109. package/dist/services/phase-hooks/__tests__/hook-runner.test.d.ts +0 -1
  110. package/dist/services/phase-hooks/__tests__/hook-runner.test.js +0 -261
  111. package/dist/services/phase-hooks/__tests__/plugin-loader.test.d.ts +0 -1
  112. package/dist/services/phase-hooks/__tests__/plugin-loader.test.js +0 -158
  113. package/dist/services/video/__tests__/video-pipeline.test.d.ts +0 -6
  114. package/dist/services/video/__tests__/video-pipeline.test.js +0 -249
  115. package/dist/workspace/__tests__/workspace-manager.test.d.ts +0 -7
  116. package/dist/workspace/__tests__/workspace-manager.test.js +0 -52
@@ -1,158 +0,0 @@
1
- import assert from 'node:assert';
2
- import { mkdir, rm, writeFile } from 'node:fs/promises';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
- import { afterEach, beforeEach, describe, it } from 'node:test';
6
- import { getPluginCachePath, loadSkillFile, resolveSkillFilePaths, } from '../plugin-loader.js';
7
- // ---- resolveSkillFilePaths ----
8
- void describe('resolveSkillFilePaths', () => {
9
- void it('returns skills/ path first, commands/ path second', () => {
10
- const paths = resolveSkillFilePaths('payload-cms', 'validate-schema');
11
- const cache = getPluginCachePath();
12
- assert.strictEqual(paths.length, 2);
13
- assert.strictEqual(paths[0], path.join(cache, 'payload-cms', 'skills', 'validate-schema', 'SKILL.md'));
14
- assert.strictEqual(paths[1], path.join(cache, 'payload-cms', 'commands', 'validate-schema.md'));
15
- });
16
- void it('uses custom cacheDir when provided', () => {
17
- const paths = resolveSkillFilePaths('my-plugin', 'my-skill', '/tmp/test-cache');
18
- assert.ok(paths[0].startsWith('/tmp/test-cache/my-plugin'));
19
- });
20
- });
21
- // ---- getPluginCachePath ----
22
- void describe('getPluginCachePath', () => {
23
- void it('returns a path under home directory', () => {
24
- const cachePath = getPluginCachePath();
25
- assert.ok(cachePath.startsWith(os.homedir()));
26
- assert.ok(cachePath.endsWith(path.join('.claude', 'plugins', 'cache')));
27
- });
28
- });
29
- // ---- loadSkillFile ----
30
- void describe('loadSkillFile', () => {
31
- let tmpDir;
32
- beforeEach(async () => {
33
- tmpDir = await import('node:fs/promises').then((fs) => fs.mkdtemp(path.join(os.tmpdir(), 'edsger-hook-test-')));
34
- });
35
- afterEach(async () => {
36
- await rm(tmpDir, { recursive: true, force: true });
37
- });
38
- // -- Flat structure: cache/{pluginName}/skills/{skill}/SKILL.md --
39
- void it('loads from flat structure', async () => {
40
- const skillDir = path.join(tmpDir, 'my-plugin', 'skills', 'check');
41
- await mkdir(skillDir, { recursive: true });
42
- await writeFile(path.join(skillDir, 'SKILL.md'), '---\nmodel: sonnet\nmaxTurns: 10\n---\nCheck the code.');
43
- const result = await loadSkillFile('my-plugin', 'check', false, tmpDir);
44
- assert.ok(result);
45
- assert.strictEqual(result.frontmatter.model, 'sonnet');
46
- assert.strictEqual(result.frontmatter.maxTurns, 10);
47
- assert.strictEqual(result.body, 'Check the code.');
48
- });
49
- // -- Nested structure: cache/{marketplace}/{plugin}/{version}/skills/... --
50
- void it('loads from nested marketplace cache structure', async () => {
51
- // Simulate: cache/edsger-local-ing/ing/1.0.0/skills/review-local/SKILL.md
52
- const skillDir = path.join(tmpDir, 'edsger-local-ing', 'ing', '1.0.0', 'skills', 'review-local');
53
- const manifestDir = path.join(tmpDir, 'edsger-local-ing', 'ing', '1.0.0', '.claude-plugin');
54
- await mkdir(skillDir, { recursive: true });
55
- await mkdir(manifestDir, { recursive: true });
56
- await writeFile(path.join(skillDir, 'SKILL.md'), '---\ndescription: Review local changes\n---\nReview the diff.');
57
- await writeFile(path.join(manifestDir, 'plugin.json'), JSON.stringify({ name: 'ing', version: '1.0.0' }));
58
- // loadSkillFile('ing', 'review-local') should find it via manifest name match
59
- const result = await loadSkillFile('ing', 'review-local', false, tmpDir);
60
- assert.ok(result, 'Should find skill in nested structure');
61
- assert.strictEqual(result.frontmatter.description, 'Review local changes');
62
- assert.strictEqual(result.body, 'Review the diff.');
63
- });
64
- // -- Fallback to commands/ --
65
- void it('falls back to commands/ when skills/ does not exist', async () => {
66
- const commandsDir = path.join(tmpDir, 'legacy-plugin', 'commands');
67
- await mkdir(commandsDir, { recursive: true });
68
- await writeFile(path.join(commandsDir, 'deploy.md'), '---\ndescription: Deploy to staging\n---\nDeploy the branch.');
69
- const result = await loadSkillFile('legacy-plugin', 'deploy', false, tmpDir);
70
- assert.ok(result);
71
- assert.strictEqual(result.frontmatter.description, 'Deploy to staging');
72
- });
73
- // -- Prefers skills/ over commands/ --
74
- void it('prefers skills/ over commands/ when both exist', async () => {
75
- const skillDir = path.join(tmpDir, 'dual-plugin', 'skills', 'check');
76
- const cmdDir = path.join(tmpDir, 'dual-plugin', 'commands');
77
- await mkdir(skillDir, { recursive: true });
78
- await mkdir(cmdDir, { recursive: true });
79
- await writeFile(path.join(skillDir, 'SKILL.md'), '---\nsource: skill\n---\nFrom skills/');
80
- await writeFile(path.join(cmdDir, 'check.md'), '---\nsource: command\n---\nFrom commands/');
81
- const result = await loadSkillFile('dual-plugin', 'check', false, tmpDir);
82
- assert.ok(result);
83
- assert.strictEqual(result.frontmatter.source, 'skill');
84
- });
85
- // -- Edge cases --
86
- void it('handles SKILL.md with no frontmatter', async () => {
87
- const skillDir = path.join(tmpDir, 'bare', 'skills', 'simple');
88
- await mkdir(skillDir, { recursive: true });
89
- await writeFile(path.join(skillDir, 'SKILL.md'), 'Plain instructions.');
90
- const result = await loadSkillFile('bare', 'simple', false, tmpDir);
91
- assert.ok(result);
92
- assert.deepStrictEqual(result.frontmatter, {});
93
- assert.strictEqual(result.body, 'Plain instructions.');
94
- });
95
- void it('handles SKILL.md with empty frontmatter', async () => {
96
- const skillDir = path.join(tmpDir, 'empty-fm', 'skills', 'e');
97
- await mkdir(skillDir, { recursive: true });
98
- await writeFile(path.join(skillDir, 'SKILL.md'), '---\n---\nBody only.');
99
- const result = await loadSkillFile('empty-fm', 'e', false, tmpDir);
100
- assert.ok(result);
101
- assert.deepStrictEqual(result.frontmatter, {});
102
- assert.strictEqual(result.body, 'Body only.');
103
- });
104
- void it('returns null when plugin does not exist', async () => {
105
- const result = await loadSkillFile('nonexistent', 'nope', false, tmpDir);
106
- assert.strictEqual(result, null);
107
- });
108
- void it('trims body whitespace', async () => {
109
- const skillDir = path.join(tmpDir, 'trim', 'skills', 'padded');
110
- await mkdir(skillDir, { recursive: true });
111
- await writeFile(path.join(skillDir, 'SKILL.md'), '---\n---\n\n Padded. \n\n');
112
- const result = await loadSkillFile('trim', 'padded', false, tmpDir);
113
- assert.ok(result);
114
- assert.strictEqual(result.body, 'Padded.');
115
- });
116
- // -- Nested structure without manifest (match by directory name) --
117
- void it('loads from nested structure when plugin dir name matches', async () => {
118
- // cache/some-marketplace/my-tool/2.0.0/skills/scan/SKILL.md
119
- const skillDir = path.join(tmpDir, 'some-marketplace', 'my-tool', '2.0.0', 'skills', 'scan');
120
- await mkdir(skillDir, { recursive: true });
121
- await writeFile(path.join(skillDir, 'SKILL.md'), '---\n---\nScan it.');
122
- // Should find via subdirectory name matching pluginName
123
- const result = await loadSkillFile('my-tool', 'scan', false, tmpDir);
124
- assert.ok(result, 'Should find skill by directory name match');
125
- assert.strictEqual(result.body, 'Scan it.');
126
- });
127
- // -- Nested structure: match by manifest name (dir name differs) --
128
- void it('loads via manifest name when cache dir name differs from plugin name', async () => {
129
- // cache/random-key/1.0.0/skills/deploy/SKILL.md
130
- // cache/random-key/1.0.0/.claude-plugin/plugin.json → { "name": "deployer" }
131
- // loadSkillFile('deployer', 'deploy') should find it by scanning manifest
132
- const pluginRoot = path.join(tmpDir, 'random-key', '1.0.0');
133
- const skillDir = path.join(pluginRoot, 'skills', 'deploy');
134
- const manifestDir = path.join(pluginRoot, '.claude-plugin');
135
- await mkdir(skillDir, { recursive: true });
136
- await mkdir(manifestDir, { recursive: true });
137
- await writeFile(path.join(skillDir, 'SKILL.md'), '---\ndescription: Deploy app\n---\nRun deploy.');
138
- await writeFile(path.join(manifestDir, 'plugin.json'), JSON.stringify({ name: 'deployer', version: '1.0.0' }));
139
- // 'deployer' doesn't appear in any directory name — only in manifest
140
- const result = await loadSkillFile('deployer', 'deploy', false, tmpDir);
141
- assert.ok(result, 'Should find skill via manifest name scan');
142
- assert.strictEqual(result.frontmatter.description, 'Deploy app');
143
- assert.strictEqual(result.body, 'Run deploy.');
144
- });
145
- void it('does not match manifest name of a different plugin', async () => {
146
- // cache/some-key/1.0.0/.claude-plugin/plugin.json → { "name": "other-plugin" }
147
- const pluginRoot = path.join(tmpDir, 'some-key', '1.0.0');
148
- const skillDir = path.join(pluginRoot, 'skills', 'action');
149
- const manifestDir = path.join(pluginRoot, '.claude-plugin');
150
- await mkdir(skillDir, { recursive: true });
151
- await mkdir(manifestDir, { recursive: true });
152
- await writeFile(path.join(skillDir, 'SKILL.md'), '---\n---\nDo something.');
153
- await writeFile(path.join(manifestDir, 'plugin.json'), JSON.stringify({ name: 'other-plugin' }));
154
- // Looking for 'wrong-name' — should NOT match 'other-plugin'
155
- const result = await loadSkillFile('wrong-name', 'action', false, tmpDir);
156
- assert.strictEqual(result, null);
157
- });
158
- });
@@ -1,6 +0,0 @@
1
- /**
2
- * Unit tests for the video generation pipeline.
3
- * Tests pure logic functions that don't require external dependencies
4
- * (Playwright, ffmpeg, TTS APIs).
5
- */
6
- export {};
@@ -1,249 +0,0 @@
1
- /**
2
- * Unit tests for the video generation pipeline.
3
- * Tests pure logic functions that don't require external dependencies
4
- * (Playwright, ffmpeg, TTS APIs).
5
- */
6
- import assert from 'node:assert';
7
- import { describe, it } from 'node:test';
8
- // ============================================================
9
- // HTML Template Validation (inline re-implementation for testing)
10
- // ============================================================
11
- function escapeHtml(str) {
12
- return str
13
- .replace(/&/g, '&')
14
- .replace(/</g, '&lt;')
15
- .replace(/>/g, '&gt;')
16
- .replace(/"/g, '&quot;')
17
- .replace(/'/g, '&#39;');
18
- }
19
- function validateHtmlTemplate(html) {
20
- if (!html || html.trim().length < 50) {
21
- return 'fallback';
22
- }
23
- const lower = html.toLowerCase();
24
- if (!lower.includes('<html') &&
25
- !lower.includes('<body') &&
26
- !lower.includes('<div')) {
27
- return 'fallback';
28
- }
29
- if (!lower.includes('<html')) {
30
- return 'wrapped';
31
- }
32
- return 'valid';
33
- }
34
- void describe('HTML Template Validation', () => {
35
- void it('should reject empty templates', () => {
36
- assert.strictEqual(validateHtmlTemplate(''), 'fallback');
37
- assert.strictEqual(validateHtmlTemplate(null), 'fallback');
38
- assert.strictEqual(validateHtmlTemplate(undefined), 'fallback');
39
- });
40
- void it('should reject templates shorter than 50 characters', () => {
41
- assert.strictEqual(validateHtmlTemplate('<div>short</div>'), 'fallback');
42
- assert.strictEqual(validateHtmlTemplate('hello world'), 'fallback');
43
- });
44
- void it('should reject templates without HTML structure', () => {
45
- const longText = 'This is just plain text without any HTML tags at all. '.repeat(3);
46
- assert.strictEqual(validateHtmlTemplate(longText), 'fallback');
47
- });
48
- void it('should wrap partial HTML (has div but no html tag)', () => {
49
- const partial = '<div class="container"><h1>Hello World</h1><p>This is a longer paragraph with enough content.</p></div>';
50
- assert.strictEqual(validateHtmlTemplate(partial), 'wrapped');
51
- });
52
- void it('should accept complete HTML documents', () => {
53
- const full = '<html><head><style>body{margin:0}</style></head><body><div>Content that is long enough</div></body></html>';
54
- assert.strictEqual(validateHtmlTemplate(full), 'valid');
55
- });
56
- void it('should accept HTML with body but no html tag as wrapped', () => {
57
- const bodyOnly = '<body><div class="app"><h1>Dashboard</h1><p>Metrics and analytics overview panel</p></div></body>';
58
- assert.strictEqual(validateHtmlTemplate(bodyOnly), 'wrapped');
59
- });
60
- });
61
- void describe('HTML Escaping', () => {
62
- void it('should escape all dangerous characters', () => {
63
- assert.strictEqual(escapeHtml('<script>alert("xss")</script>'), '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;');
64
- });
65
- void it('should escape ampersands', () => {
66
- assert.strictEqual(escapeHtml('A & B'), 'A &amp; B');
67
- });
68
- void it('should escape single quotes', () => {
69
- assert.strictEqual(escapeHtml("it's"), 'it&#39;s');
70
- });
71
- void it('should handle empty strings', () => {
72
- assert.strictEqual(escapeHtml(''), '');
73
- });
74
- void it('should not double-escape already escaped content', () => {
75
- // First escape
76
- const once = escapeHtml('<div>');
77
- assert.strictEqual(once, '&lt;div&gt;');
78
- // Double escaping should escape the ampersands
79
- const twice = escapeHtml(once);
80
- assert.strictEqual(twice, '&amp;lt;div&amp;gt;');
81
- });
82
- });
83
- // ============================================================
84
- // Retry Logic
85
- // ============================================================
86
- void describe('Retry Backoff Calculation', () => {
87
- // Re-implement the backoff formula for testing
88
- function calculateBackoff(attempt, baseDelayMs) {
89
- return baseDelayMs * Math.pow(2, attempt);
90
- }
91
- void it('should use exponential backoff', () => {
92
- assert.strictEqual(calculateBackoff(0, 1000), 1000);
93
- assert.strictEqual(calculateBackoff(1, 1000), 2000);
94
- assert.strictEqual(calculateBackoff(2, 1000), 4000);
95
- assert.strictEqual(calculateBackoff(3, 1000), 8000);
96
- });
97
- void it('should respect base delay', () => {
98
- assert.strictEqual(calculateBackoff(0, 500), 500);
99
- assert.strictEqual(calculateBackoff(1, 500), 1000);
100
- assert.strictEqual(calculateBackoff(2, 500), 2000);
101
- });
102
- });
103
- // ============================================================
104
- // Device Frame Auto-Detection
105
- // ============================================================
106
- void describe('Device Frame Auto-Detection', () => {
107
- function autoDetectDeviceFrame(width, height) {
108
- const aspectRatio = width / height;
109
- if (aspectRatio < 0.7) {
110
- return 'iphone';
111
- }
112
- return 'browser';
113
- }
114
- void it('should detect portrait mobile as iphone', () => {
115
- assert.strictEqual(autoDetectDeviceFrame(390, 844), 'iphone');
116
- assert.strictEqual(autoDetectDeviceFrame(375, 812), 'iphone');
117
- });
118
- void it('should detect landscape desktop as browser', () => {
119
- assert.strictEqual(autoDetectDeviceFrame(1280, 720), 'browser');
120
- assert.strictEqual(autoDetectDeviceFrame(1280, 800), 'browser');
121
- assert.strictEqual(autoDetectDeviceFrame(1920, 1080), 'browser');
122
- });
123
- void it('should detect square-ish ratios as browser', () => {
124
- assert.strictEqual(autoDetectDeviceFrame(800, 800), 'browser');
125
- assert.strictEqual(autoDetectDeviceFrame(1024, 768), 'browser');
126
- });
127
- });
128
- // ============================================================
129
- // Video Plan Extraction
130
- // ============================================================
131
- void describe('Video Plan Extraction', () => {
132
- function extractVideoPlans(
133
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
134
- contentSuggestions) {
135
- const plans = [];
136
- for (let i = 0; i < contentSuggestions.length; i++) {
137
- const suggestion = contentSuggestions[i];
138
- const videoPlan = suggestion.video_plan;
139
- if (videoPlan &&
140
- videoPlan.should_generate_video &&
141
- videoPlan.scenes?.length > 0) {
142
- plans.push({ index: i, shouldGenerate: true });
143
- }
144
- }
145
- return plans;
146
- }
147
- void it('should extract suggestions with video plans', () => {
148
- const suggestions = [
149
- {
150
- title: 'Blog Post',
151
- video_plan: { should_generate_video: false, scenes: [] },
152
- },
153
- {
154
- title: 'Demo Video',
155
- video_plan: { should_generate_video: true, scenes: [{ order: 1 }] },
156
- },
157
- { title: 'Tweet', video_plan: null },
158
- ];
159
- const plans = extractVideoPlans(suggestions);
160
- assert.strictEqual(plans.length, 1);
161
- assert.strictEqual(plans[0].index, 1);
162
- });
163
- void it('should ignore suggestions with empty scenes', () => {
164
- const suggestions = [
165
- {
166
- title: 'Video',
167
- video_plan: { should_generate_video: true, scenes: [] },
168
- },
169
- ];
170
- const plans = extractVideoPlans(suggestions);
171
- assert.strictEqual(plans.length, 0);
172
- });
173
- void it('should handle suggestions with no video plan', () => {
174
- const suggestions = [
175
- { title: 'Post 1' },
176
- { title: 'Post 2', video_plan: undefined },
177
- ];
178
- const plans = extractVideoPlans(suggestions);
179
- assert.strictEqual(plans.length, 0);
180
- });
181
- });
182
- // ============================================================
183
- // Concurrency Limiter
184
- // ============================================================
185
- void describe('Concurrency Limiter', () => {
186
- async function runWithConcurrencyLimit(tasks, limit) {
187
- const results = new Array(tasks.length);
188
- let nextIndex = 0;
189
- async function runNext() {
190
- while (nextIndex < tasks.length) {
191
- const i = nextIndex++;
192
- try {
193
- const value = await tasks[i]();
194
- results[i] = { status: 'fulfilled', value };
195
- }
196
- catch (reason) {
197
- results[i] = { status: 'rejected', reason };
198
- }
199
- }
200
- }
201
- const workers = Array.from({ length: Math.min(limit, tasks.length) }, () => runNext());
202
- await Promise.all(workers);
203
- return results;
204
- }
205
- void it('should process all tasks and preserve order', async () => {
206
- const tasks = [
207
- () => Promise.resolve('a'),
208
- () => Promise.resolve('b'),
209
- () => Promise.resolve('c'),
210
- ];
211
- const results = await runWithConcurrencyLimit(tasks, 2);
212
- assert.strictEqual(results.length, 3);
213
- assert.strictEqual(results[0].status, 'fulfilled');
214
- assert.strictEqual(results[0].value, 'a');
215
- assert.strictEqual(results[2].value, 'c');
216
- });
217
- void it('should handle rejected tasks without stopping others', async () => {
218
- const tasks = [
219
- () => Promise.resolve('ok'),
220
- () => Promise.reject(new Error('fail')),
221
- () => Promise.resolve('also ok'),
222
- ];
223
- const results = await runWithConcurrencyLimit(tasks, 2);
224
- assert.strictEqual(results[0].status, 'fulfilled');
225
- assert.strictEqual(results[1].status, 'rejected');
226
- assert.strictEqual(results[2].status, 'fulfilled');
227
- });
228
- void it('should respect concurrency limit', async () => {
229
- let maxConcurrent = 0;
230
- let currentConcurrent = 0;
231
- const tasks = Array.from({ length: 6 }, (_, i) => async () => {
232
- currentConcurrent++;
233
- if (currentConcurrent > maxConcurrent) {
234
- maxConcurrent = currentConcurrent;
235
- }
236
- await new Promise((r) => {
237
- setTimeout(r, 10);
238
- });
239
- currentConcurrent--;
240
- return i;
241
- });
242
- await runWithConcurrencyLimit(tasks, 2);
243
- assert.ok(maxConcurrent <= 2, `Max concurrent was ${maxConcurrent}, expected <= 2`);
244
- });
245
- void it('should handle empty task list', async () => {
246
- const results = await runWithConcurrencyLimit([], 5);
247
- assert.strictEqual(results.length, 0);
248
- });
249
- });
@@ -1,7 +0,0 @@
1
- /**
2
- * Unit tests for the pure helpers inside workspace-manager.
3
- *
4
- * syncRepoToRef / cloneFeatureRepo shell out to git and are exercised
5
- * end-to-end; only the validator is cheap to unit-test here.
6
- */
7
- export {};
@@ -1,52 +0,0 @@
1
- /**
2
- * Unit tests for the pure helpers inside workspace-manager.
3
- *
4
- * syncRepoToRef / cloneFeatureRepo shell out to git and are exercised
5
- * end-to-end; only the validator is cheap to unit-test here.
6
- */
7
- import assert from 'node:assert';
8
- import { describe, it } from 'node:test';
9
- import { isSafeGitRef } from '../workspace-manager.js';
10
- void describe('isSafeGitRef', () => {
11
- void it('accepts common release / branch names', () => {
12
- assert.strictEqual(isSafeGitRef('main'), true);
13
- assert.strictEqual(isSafeGitRef('develop'), true);
14
- assert.strictEqual(isSafeGitRef('v1.2.3'), true);
15
- assert.strictEqual(isSafeGitRef('2024.01.15'), true);
16
- assert.strictEqual(isSafeGitRef('release/2.0'), true);
17
- assert.strictEqual(isSafeGitRef('v1.0.0-rc.1'), true);
18
- assert.strictEqual(isSafeGitRef('v2@stable'), true);
19
- assert.strictEqual(isSafeGitRef('feature/user-auth_v2'), true);
20
- });
21
- void it('rejects empty / overlong input', () => {
22
- assert.strictEqual(isSafeGitRef(''), false);
23
- assert.strictEqual(isSafeGitRef('x'.repeat(101)), false);
24
- });
25
- void it('rejects whitespace', () => {
26
- assert.strictEqual(isSafeGitRef('v 1.0'), false);
27
- assert.strictEqual(isSafeGitRef('main\n'), false);
28
- assert.strictEqual(isSafeGitRef('\tmain'), false);
29
- });
30
- void it('rejects shell metacharacters', () => {
31
- assert.strictEqual(isSafeGitRef('v1;rm -rf /'), false);
32
- assert.strictEqual(isSafeGitRef('v1$PATH'), false);
33
- assert.strictEqual(isSafeGitRef('v1`id`'), false);
34
- assert.strictEqual(isSafeGitRef('v1|less'), false);
35
- assert.strictEqual(isSafeGitRef('v1>out'), false);
36
- assert.strictEqual(isSafeGitRef('v1*'), false);
37
- });
38
- void it('rejects refs that would confuse git itself', () => {
39
- assert.strictEqual(isSafeGitRef('.hidden'), false);
40
- assert.strictEqual(isSafeGitRef('-foo'), false);
41
- assert.strictEqual(isSafeGitRef('v2..rc'), false);
42
- assert.strictEqual(isSafeGitRef('HEAD@{1}'), false);
43
- });
44
- void it('rejects non-string input defensively', () => {
45
- // @ts-expect-error testing runtime guard
46
- assert.strictEqual(isSafeGitRef(null), false);
47
- // @ts-expect-error testing runtime guard
48
- assert.strictEqual(isSafeGitRef(undefined), false);
49
- // @ts-expect-error testing runtime guard
50
- assert.strictEqual(isSafeGitRef(123), false);
51
- });
52
- });