@assistkick/create 1.8.0 → 1.9.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 (24) hide show
  1. package/package.json +1 -1
  2. package/templates/assistkick-product-system/packages/backend/src/routes/kanban.ts +18 -2
  3. package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +9 -5
  4. package/templates/assistkick-product-system/packages/backend/src/server.ts +1 -22
  5. package/templates/assistkick-product-system/packages/backend/src/services/init.ts +16 -0
  6. package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.ts +30 -7
  7. package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +2 -2
  8. package/templates/assistkick-product-system/packages/frontend/src/components/IterationCommentModal.tsx +80 -0
  9. package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +67 -2
  10. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GenerateTTSNode.tsx +52 -0
  11. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RebuildBundleNode.tsx +20 -0
  12. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RenderVideoNode.tsx +72 -0
  13. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +6 -0
  14. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +9 -0
  15. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +39 -1
  16. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +30 -1
  17. package/templates/assistkick-product-system/packages/shared/db/migrations/0014_nifty_punisher.sql +15 -0
  18. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0014_snapshot.json +1545 -0
  19. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +7 -0
  20. package/templates/assistkick-product-system/packages/shared/db/schema.ts +1 -0
  21. package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +1 -1
  22. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +247 -1
  23. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +158 -2
  24. package/templates/assistkick-product-system/tests/video_render_service.test.ts +6 -4
@@ -99,6 +99,13 @@
99
99
  "when": 1773179139368,
100
100
  "tag": "0013_reflective_prowler",
101
101
  "breakpoints": true
102
+ },
103
+ {
104
+ "idx": 14,
105
+ "version": "6",
106
+ "when": 1773187819583,
107
+ "tag": "0014_nifty_punisher",
108
+ "breakpoints": true
102
109
  }
103
110
  ]
104
111
  }
@@ -188,6 +188,7 @@ export const workflows = sqliteTable('workflows', {
188
188
  description: text('description'),
189
189
  projectId: text('project_id'),
190
190
  featureType: text('feature_type'),
191
+ triggerColumn: text('trigger_column'),
191
192
  isDefault: integer('is_default').notNull().default(0),
192
193
  graphData: text('graph_data').notNull(),
193
194
  createdAt: text('created_at').notNull(),
@@ -205,7 +205,7 @@ export const emitAssistantUsage = (jsonStr, callback) => {
205
205
  * @returns {{ spawnClaude: function, spawnCommand: function }}
206
206
  */
207
207
  export const createClaudeService = ({ verbose = false, log: logFn }) => {
208
- const spawnClaude = (prompt, cwd, label = 'claude', { onToolUse, onResult, onTurnUsage, onAssistantText } = {}) => new Promise((resolve, reject) => {
208
+ const spawnClaude = (prompt, cwd, label = 'claude', { onToolUse, onResult, onTurnUsage, onAssistantText } = {} as any) => new Promise((resolve, reject) => {
209
209
  const promptPreview = prompt.slice(0, 100).replace(/\n/g, '\\n');
210
210
  logFn('CLAUDE', `Spawning ${label} in ${cwd} — prompt length: ${prompt.length} chars`);
211
211
  logFn('CLAUDE', `Prompt preview: "${promptPreview}..."`);
@@ -1,6 +1,6 @@
1
1
  import { describe, it, mock, beforeEach } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { WorkflowEngine } from './workflow_engine.ts';
3
+ import {WorkflowEngine} from "./workflow_engine.js";
4
4
 
5
5
  // ── Default workflow graph (matches seeded data) ──────────────────────
6
6
 
@@ -1750,4 +1750,250 @@ describe('WorkflowEngine', () => {
1750
1750
  assert.deepEqual(nodeExecs['transition_1'].toolCalls, []);
1751
1751
  });
1752
1752
  });
1753
+
1754
+ // ── Video Workflow Node Type Tests ────────────────────────────────
1755
+
1756
+ describe('handleRebuildBundle', () => {
1757
+ it('calls bundleService.buildBundle and returns success', async () => {
1758
+ const mockBundleService = {
1759
+ buildBundle: mock.fn(async () => ({
1760
+ ready: true,
1761
+ building: false,
1762
+ bundlePath: '/tmp/bundle',
1763
+ lastBuiltAt: '2026-03-11T00:00:00.000Z',
1764
+ error: null,
1765
+ })),
1766
+ };
1767
+
1768
+ const engine = new WorkflowEngine({
1769
+ db: createMockDb() as any,
1770
+ kanban: createMockKanban(),
1771
+ claudeService: createMockClaudeService(),
1772
+ gitWorkflow: createMockGitWorkflow(),
1773
+ bundleService: mockBundleService,
1774
+ log: createMockLog(),
1775
+ });
1776
+
1777
+ const node = { id: 'rebuild', type: 'rebuildBundle', data: {} };
1778
+ const result = await (engine as any).handleRebuildBundle(node, defaultContext());
1779
+
1780
+ assert.equal(result.outputLabel, null);
1781
+ assert.equal(result.outputData.ready, true);
1782
+ assert.equal(result.outputData.bundlePath, '/tmp/bundle');
1783
+ assert.equal(mockBundleService.buildBundle.mock.callCount(), 1);
1784
+ });
1785
+
1786
+ it('throws when bundle build fails', async () => {
1787
+ const mockBundleService = {
1788
+ buildBundle: mock.fn(async () => ({
1789
+ ready: false,
1790
+ building: false,
1791
+ bundlePath: null,
1792
+ error: 'webpack error',
1793
+ })),
1794
+ };
1795
+
1796
+ const engine = new WorkflowEngine({
1797
+ db: createMockDb() as any,
1798
+ kanban: createMockKanban(),
1799
+ claudeService: createMockClaudeService(),
1800
+ gitWorkflow: createMockGitWorkflow(),
1801
+ bundleService: mockBundleService,
1802
+ log: createMockLog(),
1803
+ });
1804
+
1805
+ const node = { id: 'rebuild', type: 'rebuildBundle', data: {} };
1806
+ await assert.rejects(
1807
+ () => (engine as any).handleRebuildBundle(node, defaultContext()),
1808
+ { message: 'Bundle build failed: webpack error' },
1809
+ );
1810
+ });
1811
+
1812
+ it('throws when bundleService is not configured', async () => {
1813
+ const engine = new WorkflowEngine({
1814
+ db: createMockDb() as any,
1815
+ kanban: createMockKanban(),
1816
+ claudeService: createMockClaudeService(),
1817
+ gitWorkflow: createMockGitWorkflow(),
1818
+ log: createMockLog(),
1819
+ });
1820
+
1821
+ const node = { id: 'rebuild', type: 'rebuildBundle', data: {} };
1822
+ await assert.rejects(
1823
+ () => (engine as any).handleRebuildBundle(node, defaultContext()),
1824
+ { message: /BundleService not configured/ },
1825
+ );
1826
+ });
1827
+ });
1828
+
1829
+ describe('handleGenerateTTS', () => {
1830
+ it('calls ttsService.generate with resolved script path', async () => {
1831
+ const mockTtsService = {
1832
+ generate: mock.fn(async () => ({
1833
+ processed: 5,
1834
+ generated: 3,
1835
+ skipped: 2,
1836
+ errors: [],
1837
+ durations: { scene1: 5.2, scene2: 3.1 },
1838
+ audioBasePath: '/tmp/audio',
1839
+ })),
1840
+ };
1841
+
1842
+ const engine = new WorkflowEngine({
1843
+ db: createMockDb() as any,
1844
+ kanban: createMockKanban(),
1845
+ claudeService: createMockClaudeService(),
1846
+ gitWorkflow: createMockGitWorkflow(),
1847
+ ttsService: mockTtsService,
1848
+ log: createMockLog(),
1849
+ });
1850
+
1851
+ const node = {
1852
+ id: 'tts',
1853
+ type: 'generateTTS',
1854
+ data: { scriptPath: 'script.md', force: true },
1855
+ };
1856
+ const ctx = defaultContext({ worktreePath: '/tmp/wt', projectId: 'proj_1' });
1857
+ const result = await (engine as any).handleGenerateTTS(node, ctx);
1858
+
1859
+ assert.equal(result.outputLabel, null);
1860
+ assert.equal(result.outputData.generated, 3);
1861
+ assert.equal(result.outputData.skipped, 2);
1862
+ assert.equal(result.outputData.audioBasePath, '/tmp/audio');
1863
+ assert.equal(mockTtsService.generate.mock.callCount(), 1);
1864
+
1865
+ const callArgs = mockTtsService.generate.mock.calls[0].arguments[0];
1866
+ assert.ok(callArgs.scriptPath.includes('script.md'));
1867
+ assert.equal(callArgs.force, true);
1868
+ assert.equal(callArgs.projectId, 'proj_1');
1869
+ assert.equal(callArgs.featureId, 'feat_test');
1870
+ });
1871
+
1872
+ it('throws when ttsService is not configured', async () => {
1873
+ const engine = new WorkflowEngine({
1874
+ db: createMockDb() as any,
1875
+ kanban: createMockKanban(),
1876
+ claudeService: createMockClaudeService(),
1877
+ gitWorkflow: createMockGitWorkflow(),
1878
+ log: createMockLog(),
1879
+ });
1880
+
1881
+ const node = { id: 'tts', type: 'generateTTS', data: {} };
1882
+ await assert.rejects(
1883
+ () => (engine as any).handleGenerateTTS(node, defaultContext()),
1884
+ { message: /TtsService not configured/ },
1885
+ );
1886
+ });
1887
+ });
1888
+
1889
+ describe('handleRenderVideo', () => {
1890
+ it('calls videoRenderService.startRender with node config', async () => {
1891
+ const mockVideoRenderService = {
1892
+ startRender: mock.fn(async () => ({
1893
+ id: 'render-123',
1894
+ status: 'queued',
1895
+ progress: 0,
1896
+ filePath: null,
1897
+ error: null,
1898
+ compositionId: 'my-comp',
1899
+ })),
1900
+ };
1901
+
1902
+ const engine = new WorkflowEngine({
1903
+ db: createMockDb() as any,
1904
+ kanban: createMockKanban(),
1905
+ claudeService: createMockClaudeService(),
1906
+ gitWorkflow: createMockGitWorkflow(),
1907
+ videoRenderService: mockVideoRenderService,
1908
+ log: createMockLog(),
1909
+ });
1910
+
1911
+ const node = {
1912
+ id: 'render',
1913
+ type: 'renderVideo',
1914
+ data: { compositionId: 'my-comp', resolution: '1280x720', fileOutputPrefix: 'output' },
1915
+ };
1916
+ const ctx = defaultContext({ projectId: 'proj_1' });
1917
+ const result = await (engine as any).handleRenderVideo(node, ctx);
1918
+
1919
+ assert.equal(result.outputLabel, null);
1920
+ assert.equal(result.outputData.renderId, 'render-123');
1921
+ assert.equal(result.outputData.status, 'queued');
1922
+ assert.equal(result.outputData.compositionId, 'my-comp');
1923
+ assert.equal(mockVideoRenderService.startRender.mock.callCount(), 1);
1924
+
1925
+ const callArgs = mockVideoRenderService.startRender.mock.calls[0].arguments[0];
1926
+ assert.equal(callArgs.compositionId, 'my-comp');
1927
+ assert.equal(callArgs.resolution, '1280x720');
1928
+ assert.equal(callArgs.fileOutputPrefix, 'output');
1929
+ });
1930
+
1931
+ it('falls back to compositionName from context when compositionId is empty', async () => {
1932
+ const mockVideoRenderService = {
1933
+ startRender: mock.fn(async () => ({
1934
+ id: 'render-456',
1935
+ status: 'queued',
1936
+ progress: 0,
1937
+ filePath: null,
1938
+ error: null,
1939
+ compositionId: 'my-feature',
1940
+ })),
1941
+ };
1942
+
1943
+ const engine = new WorkflowEngine({
1944
+ db: createMockDb() as any,
1945
+ kanban: createMockKanban(),
1946
+ claudeService: createMockClaudeService(),
1947
+ gitWorkflow: createMockGitWorkflow(),
1948
+ videoRenderService: mockVideoRenderService,
1949
+ log: createMockLog(),
1950
+ });
1951
+
1952
+ const node = { id: 'render', type: 'renderVideo', data: {} };
1953
+ const ctx = defaultContext({ projectId: 'proj_1', compositionName: 'my-feature' });
1954
+ const result = await (engine as any).handleRenderVideo(node, ctx);
1955
+
1956
+ assert.equal(result.outputData.compositionId, 'my-feature');
1957
+ const callArgs = mockVideoRenderService.startRender.mock.calls[0].arguments[0];
1958
+ assert.equal(callArgs.compositionId, 'my-feature');
1959
+ });
1960
+
1961
+ it('throws when videoRenderService is not configured', async () => {
1962
+ const engine = new WorkflowEngine({
1963
+ db: createMockDb() as any,
1964
+ kanban: createMockKanban(),
1965
+ claudeService: createMockClaudeService(),
1966
+ gitWorkflow: createMockGitWorkflow(),
1967
+ log: createMockLog(),
1968
+ });
1969
+
1970
+ const node = { id: 'render', type: 'renderVideo', data: {} };
1971
+ await assert.rejects(
1972
+ () => (engine as any).handleRenderVideo(node, defaultContext()),
1973
+ { message: /VideoRenderService not configured/ },
1974
+ );
1975
+ });
1976
+
1977
+ it('throws when no compositionId is available', async () => {
1978
+ const mockVideoRenderService = {
1979
+ startRender: mock.fn(async () => ({})),
1980
+ };
1981
+
1982
+ const engine = new WorkflowEngine({
1983
+ db: createMockDb() as any,
1984
+ kanban: createMockKanban(),
1985
+ claudeService: createMockClaudeService(),
1986
+ gitWorkflow: createMockGitWorkflow(),
1987
+ videoRenderService: mockVideoRenderService,
1988
+ log: createMockLog(),
1989
+ });
1990
+
1991
+ const node = { id: 'render', type: 'renderVideo', data: {} };
1992
+ const ctx = defaultContext({ projectId: 'proj_1' });
1993
+ await assert.rejects(
1994
+ () => (engine as any).handleRenderVideo(node, ctx),
1995
+ { message: /No compositionId configured/ },
1996
+ );
1997
+ });
1998
+ });
1753
1999
  });
@@ -63,6 +63,22 @@ interface NodeHandlerResult {
63
63
  outputData?: Record<string, unknown>;
64
64
  }
65
65
 
66
+ interface BundleServiceDep {
67
+ buildBundle: () => Promise<{ ready: boolean; building: boolean; bundlePath: string | null; error: string | null }>;
68
+ }
69
+
70
+ interface TtsServiceDep {
71
+ generate: (opts: { scriptPath: string; projectId: string; featureId: string; force?: boolean; voiceId?: string }) => Promise<{
72
+ processed: number; generated: number; skipped: number; errors: string[]; durations: Record<string, number>; audioBasePath: string;
73
+ }>;
74
+ }
75
+
76
+ interface VideoRenderServiceDep {
77
+ startRender: (request: {
78
+ compositionId: string; projectId: string; featureId: string; resolution?: string; aspectRatio?: string; fileOutputPrefix?: string;
79
+ }) => Promise<{ id: string; status: string; progress: number; filePath: string | null; error: string | null; compositionId: string }>;
80
+ }
81
+
66
82
  interface WorkflowEngineDeps {
67
83
  db: ReturnType<typeof import('./db.js').getDb>;
68
84
  kanban: {
@@ -85,6 +101,9 @@ interface WorkflowEngineDeps {
85
101
  unstash: () => Promise<void>;
86
102
  getDirtyFiles: () => Promise<string[]>;
87
103
  };
104
+ bundleService?: BundleServiceDep;
105
+ ttsService?: TtsServiceDep;
106
+ videoRenderService?: VideoRenderServiceDep;
88
107
  log: (tag: string, message: string) => void;
89
108
  }
90
109
 
@@ -95,13 +114,19 @@ export class WorkflowEngine {
95
114
  private kanban: WorkflowEngineDeps['kanban'];
96
115
  private claudeService: WorkflowEngineDeps['claudeService'];
97
116
  private gitWorkflow: WorkflowEngineDeps['gitWorkflow'];
117
+ private bundleService?: BundleServiceDep;
118
+ private ttsService?: TtsServiceDep;
119
+ private videoRenderService?: VideoRenderServiceDep;
98
120
  private log: WorkflowEngineDeps['log'];
99
121
 
100
- constructor({ db, kanban, claudeService, gitWorkflow, log }: WorkflowEngineDeps) {
122
+ constructor({ db, kanban, claudeService, gitWorkflow, bundleService, ttsService, videoRenderService, log }: WorkflowEngineDeps) {
101
123
  this.db = db;
102
124
  this.kanban = kanban;
103
125
  this.claudeService = claudeService;
104
126
  this.gitWorkflow = gitWorkflow;
127
+ this.bundleService = bundleService;
128
+ this.ttsService = ttsService;
129
+ this.videoRenderService = videoRenderService;
105
130
  this.log = log;
106
131
  }
107
132
 
@@ -159,7 +184,7 @@ export class WorkflowEngine {
159
184
  // Load iteration comments from kanban notes for re-runs
160
185
  const kanbanEntry = await this.kanban.getKanbanEntry(featureId);
161
186
  if (kanbanEntry?.notes?.length) {
162
- context.iterationComments = '## Previous Review Notes\n' + (kanbanEntry.notes as string[]).map((n: string) => `- ${n}`).join('\n');
187
+ context.iterationComments = '## Previous Review Notes\n' + (kanbanEntry.notes as Array<{ text?: string }>).map((n) => `- ${typeof n === 'string' ? n : (n.text || '')}`).join('\n');
163
188
  } else {
164
189
  context.iterationComments = '';
165
190
  }
@@ -598,6 +623,12 @@ export class WorkflowEngine {
598
623
  return this.handleEnd(node, context, executionId);
599
624
  case 'group':
600
625
  return this.handleGroup(node, context, executionId);
626
+ case 'rebuildBundle':
627
+ return this.handleRebuildBundle(node, context);
628
+ case 'generateTTS':
629
+ return this.handleGenerateTTS(node, context);
630
+ case 'renderVideo':
631
+ return this.handleRenderVideo(node, context);
601
632
  default:
602
633
  throw new Error(`Unknown node type: ${node.type}`);
603
634
  }
@@ -919,6 +950,131 @@ export class WorkflowEngine {
919
950
  return { outputLabel: null };
920
951
  };
921
952
 
953
+ /**
954
+ * RebuildBundle — triggers BundleService to compile the Remotion webpack bundle.
955
+ */
956
+ private handleRebuildBundle = async (
957
+ _node: WorkflowNode,
958
+ _context: ExecutionContext,
959
+ ): Promise<NodeHandlerResult> => {
960
+ if (!this.bundleService) {
961
+ throw new Error('BundleService not configured — cannot execute rebuildBundle node');
962
+ }
963
+
964
+ this.log('WORKFLOW', 'Building Remotion bundle...');
965
+ const result = await this.bundleService.buildBundle();
966
+
967
+ if (!result.ready) {
968
+ throw new Error(`Bundle build failed: ${result.error || 'unknown error'}`);
969
+ }
970
+
971
+ this.log('WORKFLOW', `Bundle built successfully at ${result.bundlePath}`);
972
+ return {
973
+ outputLabel: null,
974
+ outputData: { bundlePath: result.bundlePath, ready: result.ready },
975
+ };
976
+ };
977
+
978
+ /**
979
+ * GenerateTTS — runs TtsService to generate audio from a video script.
980
+ * Resolves scriptPath relative to the feature's worktree.
981
+ */
982
+ private handleGenerateTTS = async (
983
+ node: WorkflowNode,
984
+ context: ExecutionContext,
985
+ ): Promise<NodeHandlerResult> => {
986
+ if (!this.ttsService) {
987
+ throw new Error('TtsService not configured — cannot execute generateTTS node');
988
+ }
989
+
990
+ const { scriptPath, force, voiceId } = node.data as {
991
+ scriptPath?: string;
992
+ force?: boolean;
993
+ voiceId?: string;
994
+ };
995
+ const { featureId, projectId, worktreePath } = context;
996
+
997
+ // Resolve script path relative to worktree
998
+ const resolvedScriptPath = scriptPath
999
+ ? pathResolve(worktreePath as string, scriptPath)
1000
+ : pathResolve(worktreePath as string, 'script.md');
1001
+
1002
+ this.log('WORKFLOW', `Generating TTS for feature=${featureId} script=${resolvedScriptPath}`);
1003
+
1004
+ const result = await this.ttsService.generate({
1005
+ scriptPath: resolvedScriptPath,
1006
+ projectId: projectId as string,
1007
+ featureId: featureId as string,
1008
+ force: force || false,
1009
+ voiceId: voiceId || undefined,
1010
+ });
1011
+
1012
+ if (result.errors.length > 0) {
1013
+ this.log('WORKFLOW', `TTS completed with errors: ${result.errors.join(', ')}`);
1014
+ }
1015
+
1016
+ this.log('WORKFLOW', `TTS complete: ${result.generated} generated, ${result.skipped} skipped`);
1017
+ return {
1018
+ outputLabel: null,
1019
+ outputData: {
1020
+ processed: result.processed,
1021
+ generated: result.generated,
1022
+ skipped: result.skipped,
1023
+ errors: result.errors,
1024
+ audioBasePath: result.audioBasePath,
1025
+ },
1026
+ };
1027
+ };
1028
+
1029
+ /**
1030
+ * RenderVideo — triggers VideoRenderService to render a composition to MP4.
1031
+ * Waits for the render to complete by polling status.
1032
+ */
1033
+ private handleRenderVideo = async (
1034
+ node: WorkflowNode,
1035
+ context: ExecutionContext,
1036
+ ): Promise<NodeHandlerResult> => {
1037
+ if (!this.videoRenderService) {
1038
+ throw new Error('VideoRenderService not configured — cannot execute renderVideo node');
1039
+ }
1040
+
1041
+ const { compositionId, resolution, aspectRatio, fileOutputPrefix } = node.data as {
1042
+ compositionId?: string;
1043
+ resolution?: string;
1044
+ aspectRatio?: string;
1045
+ fileOutputPrefix?: string;
1046
+ };
1047
+ const { featureId, projectId, compositionName } = context;
1048
+
1049
+ // Use compositionId from node data, or fall back to context compositionName
1050
+ const resolvedCompositionId = compositionId || (compositionName as string) || '';
1051
+ if (!resolvedCompositionId) {
1052
+ throw new Error('No compositionId configured on renderVideo node and no compositionName in context');
1053
+ }
1054
+
1055
+ this.log('WORKFLOW', `Starting render: composition=${resolvedCompositionId} resolution=${resolution || '1920x1080'}`);
1056
+
1057
+ const renderStatus = await this.videoRenderService.startRender({
1058
+ compositionId: resolvedCompositionId,
1059
+ projectId: projectId as string,
1060
+ featureId: featureId as string,
1061
+ resolution,
1062
+ aspectRatio,
1063
+ fileOutputPrefix,
1064
+ });
1065
+
1066
+ this.log('WORKFLOW', `Render started: id=${renderStatus.id} status=${renderStatus.status}`);
1067
+ return {
1068
+ outputLabel: null,
1069
+ outputData: {
1070
+ renderId: renderStatus.id,
1071
+ status: renderStatus.status,
1072
+ compositionId: resolvedCompositionId,
1073
+ filePath: renderStatus.filePath,
1074
+ },
1075
+ };
1076
+ };
1077
+
922
1078
  // ── Merge & Cleanup Helpers ─────────────────────────────────────────
923
1079
 
924
1080
  /**
@@ -19,6 +19,7 @@ describe('VideoRenderService', () => {
19
19
  let routeContent: string;
20
20
  let schemaContent: string;
21
21
  let serverContent: string;
22
+ let initContent: string;
22
23
  let dockerfileContent: string;
23
24
 
24
25
  it('loads source files', async () => {
@@ -26,6 +27,7 @@ describe('VideoRenderService', () => {
26
27
  routeContent = await readFile(join(BACKEND_SRC, 'routes', 'video.ts'), 'utf-8');
27
28
  schemaContent = await readFile(join(SHARED_DB, 'schema.ts'), 'utf-8');
28
29
  serverContent = await readFile(join(BACKEND_SRC, 'server.ts'), 'utf-8');
30
+ initContent = await readFile(join(BACKEND_SRC, 'services', 'init.ts'), 'utf-8');
29
31
  dockerfileContent = await readFile(join(SOURCE_ROOT, '..', 'Dockerfile'), 'utf-8');
30
32
  assert.ok(serviceContent.length > 0);
31
33
  assert.ok(routeContent.length > 0);
@@ -127,12 +129,12 @@ describe('VideoRenderService', () => {
127
129
  });
128
130
 
129
131
  describe('Server integration', () => {
130
- it('imports VideoRenderService', () => {
131
- assert.ok(serverContent.includes("import { VideoRenderService }"), 'server should import VideoRenderService');
132
+ it('imports VideoRenderService in init.ts', () => {
133
+ assert.ok(initContent.includes("import { VideoRenderService }"), 'init.ts should import VideoRenderService');
132
134
  });
133
135
 
134
- it('instantiates VideoRenderService with DI', () => {
135
- assert.ok(serverContent.includes('new VideoRenderService('), 'server should instantiate VideoRenderService');
136
+ it('instantiates VideoRenderService with DI in init.ts', () => {
137
+ assert.ok(initContent.includes('new VideoRenderService('), 'init.ts should instantiate VideoRenderService');
136
138
  });
137
139
 
138
140
  it('passes videoRenderService to video routes', () => {