@xdelivered/emberflow 0.4.0 → 0.5.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.
- package/dist/server/index.js +23 -2
- package/dist/server/nodesPayload.d.ts +14 -1
- package/dist/server/nodesPayload.js +15 -2
- package/dist/server/projectMode.js +5 -2
- package/dist/server/sourceNav.d.ts +77 -0
- package/dist/server/sourceNav.js +503 -0
- package/dist/src/engine/registry.d.ts +27 -2
- package/dist/src/engine/registry.js +84 -2
- package/dist/src/nodes/index.d.ts +8 -2
- package/dist/src/nodes/index.js +7 -3
- package/dist/src/nodes/login.d.ts +3 -1
- package/dist/src/nodes/login.js +2 -2
- package/package.json +2 -2
- package/server/index.ts +24 -2
- package/server/nodesPayload.test.ts +38 -0
- package/server/nodesPayload.ts +28 -3
- package/server/projectMode.ts +5 -2
- package/server/sourceNav.test.ts +382 -0
- package/server/sourceNav.ts +644 -0
- package/server/sourceNavRoute.test.ts +163 -0
- package/src/components/Inspector.tsx +9 -26
- package/src/components/SourceNavigator.test.tsx +226 -0
- package/src/components/SourceNavigator.tsx +520 -0
- package/src/engine/registry.sourceRef.test.ts +112 -0
- package/src/engine/registry.ts +103 -2
- package/src/nodes/index.ts +10 -3
- package/src/nodes/login.ts +5 -2
- package/src/store/builderStore.ts +2 -2
- package/src/store/nodeMeta.ts +10 -2
- package/src/store/sourceNavClient.ts +48 -0
- package/studio-dist/assets/{index-CGwEx82J.js → index-BbzppDpt.js} +25 -25
- package/studio-dist/assets/{index-CAIDjNhv.css → index-CLf6blcA.css} +1 -1
- package/studio-dist/index.html +2 -2
- package/templates/skills/emberflow-basics/SKILL.md +29 -1
- package/templates/skills/emberflow-model-process/SKILL.md +11 -1
- package/templates/skills/emberflow-new-workflow/SKILL.md +8 -1
- package/templates/skills/emberflow-review-workflow/SKILL.md +28 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
3
|
+
import { mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
// End-to-end coverage of GET /source-file plus the nodesPayload sourceRef
|
|
8
|
+
// wiring, through the real runner subprocess (same harness as
|
|
9
|
+
// diagnosticsRoute.test.ts). Uses the regression fixture shape:
|
|
10
|
+
// DeriveShipmentActuals registered from nodes.mjs, whose handler imports
|
|
11
|
+
// deriveTrackingActual from shipment-logic.mjs.
|
|
12
|
+
|
|
13
|
+
let proc: ChildProcess;
|
|
14
|
+
const PORT = 8157;
|
|
15
|
+
const base = `http://127.0.0.1:${PORT}`;
|
|
16
|
+
let projectDir: string;
|
|
17
|
+
|
|
18
|
+
async function waitHealthy(url: string, tries = 40): Promise<void> {
|
|
19
|
+
for (let i = 0; i < tries; i++) {
|
|
20
|
+
try {
|
|
21
|
+
if ((await fetch(url)).ok) return;
|
|
22
|
+
} catch {
|
|
23
|
+
/* not up yet */
|
|
24
|
+
}
|
|
25
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
26
|
+
}
|
|
27
|
+
throw new Error('runner did not become healthy');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sourceFileUrl(path: string): string {
|
|
31
|
+
return `${base}/api/source-file?${new URLSearchParams({ path })}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
beforeAll(async () => {
|
|
35
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'sourcenavroute-')));
|
|
36
|
+
const w = (rel: string, content: string): void => writeFileSync(join(projectDir, rel), content);
|
|
37
|
+
w(
|
|
38
|
+
'shipment-logic.mjs',
|
|
39
|
+
[
|
|
40
|
+
'function nestedHelper(shipment) {', // 1
|
|
41
|
+
' return (shipment.weight ?? 0) * 2;', // 2
|
|
42
|
+
'}', // 3
|
|
43
|
+
'', // 4
|
|
44
|
+
'export function deriveTrackingActual(shipment) {', // 5
|
|
45
|
+
' return nestedHelper(shipment);', // 6
|
|
46
|
+
'}', // 7
|
|
47
|
+
'',
|
|
48
|
+
].join('\n'),
|
|
49
|
+
);
|
|
50
|
+
w(
|
|
51
|
+
'nodes.mjs',
|
|
52
|
+
[
|
|
53
|
+
"import { deriveTrackingActual } from './shipment-logic.mjs';", // 1
|
|
54
|
+
'', // 2
|
|
55
|
+
'export function registerNodes(registry) {', // 3
|
|
56
|
+
' registry.register(', // 4
|
|
57
|
+
" { type: 'DeriveShipmentActuals', label: 'Derive Shipment Actuals' },", // 5
|
|
58
|
+
' async (ctx) => ({ actual: deriveTrackingActual(ctx.input) }),', // 6
|
|
59
|
+
' );', // 7
|
|
60
|
+
'}', // 8
|
|
61
|
+
'',
|
|
62
|
+
].join('\n'),
|
|
63
|
+
);
|
|
64
|
+
w(
|
|
65
|
+
'emberflow.config.mjs',
|
|
66
|
+
["import { registerNodes } from './nodes.mjs';", 'export default { registerNodes };', ''].join(
|
|
67
|
+
'\n',
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
w('.env', 'TOP_SECRET=shh\n');
|
|
71
|
+
|
|
72
|
+
proc = spawn('npx', ['tsx', 'server/index.ts'], {
|
|
73
|
+
env: {
|
|
74
|
+
...process.env,
|
|
75
|
+
EMBERFLOW_RUNNER_PORT: String(PORT),
|
|
76
|
+
EMBERFLOW_PROJECT: projectDir,
|
|
77
|
+
},
|
|
78
|
+
stdio: 'ignore',
|
|
79
|
+
});
|
|
80
|
+
await waitHealthy(`${base}/healthz`);
|
|
81
|
+
}, 20_000);
|
|
82
|
+
|
|
83
|
+
afterAll(() => {
|
|
84
|
+
proc?.kill();
|
|
85
|
+
if (projectDir) rmSync(projectDir, { recursive: true, force: true });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('GET /nodes sourceRef wiring', () => {
|
|
89
|
+
it('carries a repo-relative sourceRef for the project node and builtin flag for package nodes', async () => {
|
|
90
|
+
const res = await fetch(`${base}/api/nodes`);
|
|
91
|
+
expect(res.status).toBe(200);
|
|
92
|
+
const body = (await res.json()) as {
|
|
93
|
+
nodes: Array<{ type: string; sourceRef?: { file: string; line?: number }; builtin?: boolean }>;
|
|
94
|
+
};
|
|
95
|
+
const project = body.nodes.find((n) => n.type === 'DeriveShipmentActuals');
|
|
96
|
+
expect(project).toBeDefined();
|
|
97
|
+
expect(project!.sourceRef).toEqual({ file: 'nodes.mjs', line: 4 });
|
|
98
|
+
expect(project!.builtin).toBeUndefined();
|
|
99
|
+
|
|
100
|
+
const builtin = body.nodes.find((n) => n.type === 'ValidateCredentials');
|
|
101
|
+
expect(builtin).toBeDefined();
|
|
102
|
+
expect(builtin!.builtin).toBe(true);
|
|
103
|
+
expect(builtin!.sourceRef).toBeUndefined();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('GET /source-file', () => {
|
|
108
|
+
it('serves the handler module with the imported helper resolved to its project file', async () => {
|
|
109
|
+
const res = await fetch(sourceFileUrl('nodes.mjs'));
|
|
110
|
+
expect(res.status).toBe(200);
|
|
111
|
+
const body = (await res.json()) as {
|
|
112
|
+
path: string;
|
|
113
|
+
content: string;
|
|
114
|
+
language: string;
|
|
115
|
+
symbols: {
|
|
116
|
+
imports: Array<{ local: string; resolution: { kind: string; path?: string; line?: number } }>;
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
expect(body.path).toBe('nodes.mjs');
|
|
120
|
+
expect(body.language).toBe('js');
|
|
121
|
+
expect(body.content).toContain('DeriveShipmentActuals');
|
|
122
|
+
const imp = body.symbols.imports.find((i) => i.local === 'deriveTrackingActual');
|
|
123
|
+
expect(imp!.resolution).toEqual({ kind: 'project', path: 'shipment-logic.mjs', line: 5 });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('exposes declaration lines on the helper module', async () => {
|
|
127
|
+
const res = await fetch(sourceFileUrl('shipment-logic.mjs'));
|
|
128
|
+
expect(res.status).toBe(200);
|
|
129
|
+
const body = (await res.json()) as {
|
|
130
|
+
symbols: {
|
|
131
|
+
declarations: Array<{ name: string; line: number; endLine: number; exported: boolean }>;
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
const derive = body.symbols.declarations.find((d) => d.name === 'deriveTrackingActual');
|
|
135
|
+
expect(derive).toMatchObject({ line: 5, endLine: 7, exported: true });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('400s on a traversal path without echoing it', async () => {
|
|
139
|
+
const res = await fetch(sourceFileUrl('../../etc/passwd'));
|
|
140
|
+
expect(res.status).toBe(400);
|
|
141
|
+
const body = (await res.json()) as { error: string };
|
|
142
|
+
expect(body.error).not.toContain('passwd');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('400s on secret basenames and node_modules paths', async () => {
|
|
146
|
+
for (const p of ['.env', 'node_modules/x/index.js']) {
|
|
147
|
+
const res = await fetch(sourceFileUrl(p));
|
|
148
|
+
expect(res.status).toBe(400);
|
|
149
|
+
const body = (await res.json()) as { error: string };
|
|
150
|
+
expect(body.error).not.toContain(p);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('400s when the path param is missing', async () => {
|
|
155
|
+
const res = await fetch(`${base}/api/source-file`);
|
|
156
|
+
expect(res.status).toBe(400);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('404s for a missing file inside the project', async () => {
|
|
160
|
+
const res = await fetch(sourceFileUrl('missing.mjs'));
|
|
161
|
+
expect(res.status).toBe(404);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -4,8 +4,7 @@ import { BranchRulesEditor } from './BranchRulesEditor';
|
|
|
4
4
|
import { ExecutionPager } from './ExecutionPager';
|
|
5
5
|
import { NodeRunModal } from './NodeRunModal';
|
|
6
6
|
import { Json } from './Json';
|
|
7
|
-
import {
|
|
8
|
-
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
|
7
|
+
import { NodeCodeView } from './SourceNavigator';
|
|
9
8
|
import { Badge } from '@/components/ui/badge';
|
|
10
9
|
import { Button } from '@/components/ui/button';
|
|
11
10
|
import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@/components/ui/dialog';
|
|
@@ -781,6 +780,8 @@ export function Inspector() {
|
|
|
781
780
|
const outputValue = state?.output as Record<string, unknown> | undefined;
|
|
782
781
|
const executions = state?.executions;
|
|
783
782
|
const source = registry.has(node.type) ? (registry.getSource(node.type) ?? '') : '';
|
|
783
|
+
const sourceRef = registry.has(node.type) ? registry.getSourceRef(node.type) : undefined;
|
|
784
|
+
const builtinNode = registry.has(node.type) ? registry.isBuiltin(node.type) : false;
|
|
784
785
|
|
|
785
786
|
const codeDialog = (
|
|
786
787
|
<Dialog open={codeOpen} onOpenChange={setCodeOpen}>
|
|
@@ -790,30 +791,12 @@ export function Inspector() {
|
|
|
790
791
|
<Badge variant="mono">{node.type}</Badge>
|
|
791
792
|
</DialogTitle>
|
|
792
793
|
<DialogDescription>{definition?.description ?? 'Node implementation'}</DialogDescription>
|
|
793
|
-
<
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
margin: 0,
|
|
800
|
-
background: 'transparent',
|
|
801
|
-
fontSize: '12.5px',
|
|
802
|
-
lineHeight: 1.6,
|
|
803
|
-
padding: '12px 2px',
|
|
804
|
-
}}
|
|
805
|
-
codeTagProps={{ style: { background: 'transparent', textShadow: 'none' } }}
|
|
806
|
-
lineNumberStyle={{
|
|
807
|
-
color: 'var(--muted-foreground)',
|
|
808
|
-
opacity: 0.35,
|
|
809
|
-
background: 'transparent',
|
|
810
|
-
minWidth: '2.75em',
|
|
811
|
-
paddingRight: '1em',
|
|
812
|
-
}}
|
|
813
|
-
>
|
|
814
|
-
{source}
|
|
815
|
-
</SyntaxHighlighter>
|
|
816
|
-
</div>
|
|
794
|
+
<NodeCodeView
|
|
795
|
+
nodeType={node.type}
|
|
796
|
+
source={source}
|
|
797
|
+
sourceRef={sourceRef}
|
|
798
|
+
builtin={builtinNode}
|
|
799
|
+
/>
|
|
817
800
|
</DialogContent>
|
|
818
801
|
</Dialog>
|
|
819
802
|
);
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
3
|
+
import { NodeCodeView, SourceNavigator } from './SourceNavigator';
|
|
4
|
+
import type { SourceFileFetchResult, SourceFilePayload } from '../store/sourceNavClient';
|
|
5
|
+
|
|
6
|
+
// Props-driven fixtures: renderToStaticMarkup never runs effects, so the
|
|
7
|
+
// fetch cache is pre-populated via initialFiles and the fetcher is inert.
|
|
8
|
+
const neverFetch = () => new Promise<SourceFileFetchResult>(() => {});
|
|
9
|
+
|
|
10
|
+
const NODES_MJS: SourceFilePayload = {
|
|
11
|
+
path: 'nodes.mjs',
|
|
12
|
+
content: [
|
|
13
|
+
"import { deriveTrackingActual } from './shipment-logic.mjs';",
|
|
14
|
+
"import express from 'express';",
|
|
15
|
+
"import { createHash } from 'node:crypto';",
|
|
16
|
+
'const RATE = 1.2;',
|
|
17
|
+
'export function handler(input) {',
|
|
18
|
+
' return deriveTrackingActual(input) * RATE;',
|
|
19
|
+
'}',
|
|
20
|
+
].join('\n'),
|
|
21
|
+
language: 'js',
|
|
22
|
+
symbols: {
|
|
23
|
+
declarations: [
|
|
24
|
+
{ name: 'RATE', kind: 'const', line: 4, endLine: 4, exported: false },
|
|
25
|
+
{ name: 'handler', kind: 'fn', line: 5, endLine: 7, exported: true },
|
|
26
|
+
],
|
|
27
|
+
imports: [
|
|
28
|
+
{
|
|
29
|
+
name: 'deriveTrackingActual',
|
|
30
|
+
local: 'deriveTrackingActual',
|
|
31
|
+
from: './shipment-logic.mjs',
|
|
32
|
+
resolution: { kind: 'project', path: 'shipment-logic.mjs', line: 5 },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'default',
|
|
36
|
+
local: 'express',
|
|
37
|
+
from: 'express',
|
|
38
|
+
resolution: { kind: 'external', package: 'express' },
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'createHash',
|
|
42
|
+
local: 'createHash',
|
|
43
|
+
from: 'node:crypto',
|
|
44
|
+
resolution: { kind: 'builtin' },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'import()',
|
|
48
|
+
local: '',
|
|
49
|
+
from: 'x',
|
|
50
|
+
resolution: { kind: 'unresolved', reason: 'dynamic import target is computed' },
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: '*',
|
|
54
|
+
local: '',
|
|
55
|
+
from: './polyfill.mjs',
|
|
56
|
+
resolution: { kind: 'project', path: 'polyfill.mjs' },
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
reexports: [],
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const SHIPMENT_MJS: SourceFilePayload = {
|
|
64
|
+
path: 'shipment-logic.mjs',
|
|
65
|
+
content: 'export function deriveTrackingActual(input) { return input.actual; }',
|
|
66
|
+
language: 'js',
|
|
67
|
+
symbols: {
|
|
68
|
+
declarations: [
|
|
69
|
+
{ name: 'deriveTrackingActual', kind: 'fn', line: 1, endLine: 1, exported: true },
|
|
70
|
+
],
|
|
71
|
+
imports: [],
|
|
72
|
+
reexports: [],
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const files = (payload: SourceFilePayload): Record<string, SourceFileFetchResult> => ({
|
|
77
|
+
[payload.path]: { ok: true, payload },
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/** Highlighting splits code across token spans; strip tags to assert on the raw text. */
|
|
81
|
+
const text = (html: string): string => html.replace(/<[^>]+>/g, '');
|
|
82
|
+
|
|
83
|
+
describe('SourceNavigator', () => {
|
|
84
|
+
it('renders the file content with local declarations linkified for same-file navigation', () => {
|
|
85
|
+
const out = renderToStaticMarkup(
|
|
86
|
+
<SourceNavigator
|
|
87
|
+
entryFile="nodes.mjs"
|
|
88
|
+
entryLine={5}
|
|
89
|
+
nodeType="DeriveShipmentActuals"
|
|
90
|
+
fetcher={neverFetch}
|
|
91
|
+
initialFiles={files(NODES_MJS)}
|
|
92
|
+
/>,
|
|
93
|
+
);
|
|
94
|
+
expect(out).toContain('deriveTrackingActual');
|
|
95
|
+
expect(out).toContain('RATE');
|
|
96
|
+
// Declaration + import identifiers render as link buttons (token linking).
|
|
97
|
+
expect(out).toContain('source-nav-link');
|
|
98
|
+
expect(out).toContain('Jump to line 5'); // handler declaration
|
|
99
|
+
expect(out).toContain('Open shipment-logic.mjs:5'); // project import token
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('renders a project import as a clickable path:line link in the Referenced code panel', () => {
|
|
103
|
+
const out = renderToStaticMarkup(
|
|
104
|
+
<SourceNavigator
|
|
105
|
+
entryFile="nodes.mjs"
|
|
106
|
+
nodeType="DeriveShipmentActuals"
|
|
107
|
+
fetcher={neverFetch}
|
|
108
|
+
initialFiles={files(NODES_MJS)}
|
|
109
|
+
/>,
|
|
110
|
+
);
|
|
111
|
+
expect(out).toContain('Referenced code');
|
|
112
|
+
expect(out).toContain('shipment-logic.mjs:5');
|
|
113
|
+
expect(out).toContain('project');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('badges external, builtin, and unresolved (with reason) references', () => {
|
|
117
|
+
const out = renderToStaticMarkup(
|
|
118
|
+
<SourceNavigator
|
|
119
|
+
entryFile="nodes.mjs"
|
|
120
|
+
nodeType="DeriveShipmentActuals"
|
|
121
|
+
fetcher={neverFetch}
|
|
122
|
+
initialFiles={files(NODES_MJS)}
|
|
123
|
+
/>,
|
|
124
|
+
);
|
|
125
|
+
expect(out).toContain('package: express');
|
|
126
|
+
expect(out).toContain('Node.js builtin');
|
|
127
|
+
expect(out).toContain('unresolved — dynamic import target is computed');
|
|
128
|
+
// Imports without a local binding are still listed: dynamic + side-effect.
|
|
129
|
+
expect(out).toContain('import()');
|
|
130
|
+
expect(out).toContain('(side-effect import)');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('shows the breadcrumb trail and a Back button at depth > 1', () => {
|
|
134
|
+
const out = renderToStaticMarkup(
|
|
135
|
+
<SourceNavigator
|
|
136
|
+
entryFile="nodes.mjs"
|
|
137
|
+
nodeType="DeriveShipmentActuals"
|
|
138
|
+
fetcher={neverFetch}
|
|
139
|
+
initialStack={[{ path: 'nodes.mjs', line: 5 }, { path: 'shipment-logic.mjs', line: 5 }]}
|
|
140
|
+
initialFiles={{ ...files(NODES_MJS), ...files(SHIPMENT_MJS) }}
|
|
141
|
+
/>,
|
|
142
|
+
);
|
|
143
|
+
expect(out).toContain('nodes.mjs');
|
|
144
|
+
expect(out).toContain('shipment-logic.mjs');
|
|
145
|
+
expect(out).toContain('›');
|
|
146
|
+
expect(out).toContain('Back');
|
|
147
|
+
// Current file's content is on screen, not the ancestor's.
|
|
148
|
+
expect(text(out)).toContain('return input.actual;');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('hides Back at depth 1', () => {
|
|
152
|
+
const out = renderToStaticMarkup(
|
|
153
|
+
<SourceNavigator
|
|
154
|
+
entryFile="nodes.mjs"
|
|
155
|
+
nodeType="DeriveShipmentActuals"
|
|
156
|
+
fetcher={neverFetch}
|
|
157
|
+
initialFiles={files(NODES_MJS)}
|
|
158
|
+
/>,
|
|
159
|
+
);
|
|
160
|
+
expect(out).not.toContain('Back');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('shows the resolver-unavailable banner while still rendering content', () => {
|
|
164
|
+
const degraded: SourceFilePayload = {
|
|
165
|
+
...SHIPMENT_MJS,
|
|
166
|
+
resolver: 'unavailable',
|
|
167
|
+
symbols: { declarations: [], imports: [], reexports: [] },
|
|
168
|
+
};
|
|
169
|
+
const out = renderToStaticMarkup(
|
|
170
|
+
<SourceNavigator
|
|
171
|
+
entryFile="shipment-logic.mjs"
|
|
172
|
+
nodeType="DeriveShipmentActuals"
|
|
173
|
+
fetcher={neverFetch}
|
|
174
|
+
initialFiles={files(degraded)}
|
|
175
|
+
/>,
|
|
176
|
+
);
|
|
177
|
+
expect(out).toContain('Symbol resolution unavailable (typescript not installed)');
|
|
178
|
+
expect(text(out)).toContain('return input.actual;');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('renders the fetch error text (denied / missing / runner down) instead of code', () => {
|
|
182
|
+
const out = renderToStaticMarkup(
|
|
183
|
+
<SourceNavigator
|
|
184
|
+
entryFile=".env"
|
|
185
|
+
nodeType="DeriveShipmentActuals"
|
|
186
|
+
fetcher={neverFetch}
|
|
187
|
+
initialFiles={{ '.env': { ok: false, error: 'Access denied: path not servable' } }}
|
|
188
|
+
/>,
|
|
189
|
+
);
|
|
190
|
+
expect(out).toContain('Access denied: path not servable');
|
|
191
|
+
expect(out).not.toContain('Referenced code');
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('NodeCodeView (Inspector code dialog body)', () => {
|
|
196
|
+
it('without sourceRef renders exactly the legacy highlighter view — no Referenced code', () => {
|
|
197
|
+
const out = renderToStaticMarkup(
|
|
198
|
+
<NodeCodeView nodeType="Map" source="async (input) => input" fetcher={neverFetch} />,
|
|
199
|
+
);
|
|
200
|
+
expect(out).toContain('input');
|
|
201
|
+
expect(out).not.toContain('Referenced code');
|
|
202
|
+
expect(out).not.toContain('Built-in node');
|
|
203
|
+
expect(out).not.toContain('source-nav-link');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('builtin nodes keep the legacy view plus a quiet Built-in node badge', () => {
|
|
207
|
+
const out = renderToStaticMarkup(
|
|
208
|
+
<NodeCodeView nodeType="Input" source="async () => ({})" builtin fetcher={neverFetch} />,
|
|
209
|
+
);
|
|
210
|
+
expect(out).toContain('Built-in node');
|
|
211
|
+
expect(out).not.toContain('Referenced code');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('with a sourceRef renders the SourceNavigator (breadcrumb + loading state pre-fetch)', () => {
|
|
215
|
+
const out = renderToStaticMarkup(
|
|
216
|
+
<NodeCodeView
|
|
217
|
+
nodeType="DeriveShipmentActuals"
|
|
218
|
+
source="ignored"
|
|
219
|
+
sourceRef={{ file: 'nodes.mjs', line: 4 }}
|
|
220
|
+
fetcher={neverFetch}
|
|
221
|
+
/>,
|
|
222
|
+
);
|
|
223
|
+
expect(out).toContain('nodes.mjs');
|
|
224
|
+
expect(out).toContain('Loading source…');
|
|
225
|
+
});
|
|
226
|
+
});
|