@finebi/finebi-cli 1.6.1 → 1.6.2

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 (152) hide show
  1. package/README.md +8 -8
  2. package/package.json +1 -1
  3. package/skills/finebi-cli-agent/SKILL.md +43 -123
  4. package/skills/finebi-cli-agent/agents/openai.yaml +1 -1
  5. package/skills/finebi-cli-agent/references/artifact-ordering.md +3 -3
  6. package/skills/finebi-cli-agent/references/command-workflows.md +30 -1
  7. package/skills/finebi-cli-agent/references/dashboard-data-result.md +191 -191
  8. package/skills/finebi-cli-agent/references/dashboard-dsl-api.md +44 -44
  9. package/skills/finebi-cli-agent/references/dsl-handoff-contract.md +1 -1
  10. package/skills/finebi-cli-agent/references/handoff-contract.md +6 -4
  11. package/skills/finebi-cli-agent/references/mutation-confirmation-contract.md +2 -0
  12. package/skills/finebi-cli-agent/references/scenario-workflows.md +10 -8
  13. package/skills/finebi-cli-agent/references/web-app.md +4 -2
  14. package/skills/finebi-cli-agent/references/widget-dsl-core.md +6 -0
  15. package/skills/finebi-cli-agent/scripts/finebi-cli/package.json +1 -1
  16. package/skills/finebi-cli-agent/scripts/finebi-cli/src/app.js +157 -148
  17. package/skills/finebi-cli-agent/scripts/finebi-cli/src/browser-auth.js +3 -4
  18. package/skills/finebi-cli-agent/scripts/finebi-cli/src/config.js +28 -28
  19. package/skills/finebi-cli-agent/scripts/finebi-cli/src/error-catalog.js +2 -2
  20. package/skills/finebi-cli-agent/scripts/finebi-cli/src/help.js +63 -60
  21. package/skills/finebi-cli-agent/scripts/finebi-cli/src/install-skill.js +89 -89
  22. package/skills/finebi-cli-agent/scripts/finebi-cli/src/profiles.js +158 -158
  23. package/skills/finebi-cli-agent/scripts/finebi-cli/src/skill-files.js +94 -94
  24. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/AGENTS.md +18 -5
  25. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/README.md +12 -5
  26. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/acceptance.md +80 -67
  27. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/auth-and-deploy.md +17 -2
  28. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/data-contract.md +66 -38
  29. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/filters.md +2 -2
  30. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/cockpit.config.json +22 -22
  31. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/retail.config.json +185 -185
  32. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/index.html +2 -2
  33. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/public/app-config.json +11 -11
  34. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/scripts/browser-session.mjs +59 -0
  35. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/scripts/dev-proxy.mjs +25 -24
  36. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/scripts/dev-session.mjs +57 -0
  37. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/scripts/session-file.mjs +45 -0
  38. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/App.tsx +63 -56
  39. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/cockpit.css +3 -3
  40. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Chart.tsx +40 -40
  41. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/DataTable.tsx +1 -1
  42. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Insights.tsx +17 -17
  43. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/MonthPicker.tsx +32 -32
  44. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/OptionFilter.tsx +7 -7
  45. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/RangeFilter.tsx +9 -9
  46. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Widget.tsx +40 -39
  47. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar-locales.ts +3 -3
  48. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar.ts +14 -14
  49. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Bar.tsx +6 -6
  50. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Line.tsx +6 -6
  51. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Pie.tsx +6 -6
  52. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/month-picker.css +1 -1
  53. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/client.ts +20 -4
  54. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.d.ts +3 -1
  55. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.js +58 -44
  56. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.d.ts +1 -1
  57. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.js +5 -5
  58. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.d.ts +4 -4
  59. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.js +106 -106
  60. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/normalize.ts +235 -77
  61. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/types.ts +20 -18
  62. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/main.tsx +5 -5
  63. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/retail.css +12 -12
  64. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/presentation.ts +36 -36
  65. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/table-data.ts +47 -47
  66. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/view-data.ts +18 -18
  67. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/widget-queries.ts +18 -18
  68. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/fixtures.mjs +28 -28
  69. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/index.html +16 -16
  70. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/main.tsx +149 -149
  71. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/test-page.css +4 -4
  72. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/vite.config.mjs +13 -13
  73. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/bundle.test.mjs +19 -19
  74. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/cockpit.test.mjs +73 -73
  75. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/field-selection.test.mjs +70 -0
  76. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/proxy.test.mjs +20 -5
  77. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/retail.test.mjs +31 -31
  78. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/runtime.test.mjs +7 -0
  79. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/session.test.mjs +91 -0
  80. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/table-normalize.test.mjs +189 -0
  81. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/vite-auth.test.mjs +41 -0
  82. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tsconfig.json +4 -4
  83. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/vite.config.ts +9 -4
  84. package/skills/finebi-cli-agent/scripts/finebi-cli-runner.mjs +4 -4
  85. package/src/app.js +157 -148
  86. package/src/browser-auth.js +3 -4
  87. package/src/config.js +28 -28
  88. package/src/error-catalog.js +2 -2
  89. package/src/help.js +63 -60
  90. package/src/install-skill.js +89 -89
  91. package/src/profiles.js +158 -158
  92. package/src/skill-files.js +94 -94
  93. package/templates/web-app/AGENTS.md +18 -5
  94. package/templates/web-app/README.md +12 -5
  95. package/templates/web-app/docs/acceptance.md +80 -67
  96. package/templates/web-app/docs/auth-and-deploy.md +17 -2
  97. package/templates/web-app/docs/data-contract.md +66 -38
  98. package/templates/web-app/docs/filters.md +2 -2
  99. package/templates/web-app/examples/cockpit.config.json +22 -22
  100. package/templates/web-app/examples/retail.config.json +185 -185
  101. package/templates/web-app/index.html +2 -2
  102. package/templates/web-app/public/app-config.json +11 -11
  103. package/templates/web-app/scripts/browser-session.mjs +59 -0
  104. package/templates/web-app/scripts/dev-proxy.mjs +25 -24
  105. package/templates/web-app/scripts/dev-session.mjs +57 -0
  106. package/templates/web-app/scripts/session-file.mjs +45 -0
  107. package/templates/web-app/src/App.tsx +63 -56
  108. package/templates/web-app/src/cockpit.css +3 -3
  109. package/templates/web-app/src/components/Chart.tsx +40 -40
  110. package/templates/web-app/src/components/DataTable.tsx +1 -1
  111. package/templates/web-app/src/components/Insights.tsx +17 -17
  112. package/templates/web-app/src/components/MonthPicker.tsx +32 -32
  113. package/templates/web-app/src/components/OptionFilter.tsx +7 -7
  114. package/templates/web-app/src/components/RangeFilter.tsx +9 -9
  115. package/templates/web-app/src/components/Widget.tsx +40 -39
  116. package/templates/web-app/src/components/calendar-locales.ts +3 -3
  117. package/templates/web-app/src/components/calendar.ts +14 -14
  118. package/templates/web-app/src/components/charts/Bar.tsx +6 -6
  119. package/templates/web-app/src/components/charts/Line.tsx +6 -6
  120. package/templates/web-app/src/components/charts/Pie.tsx +6 -6
  121. package/templates/web-app/src/components/month-picker.css +1 -1
  122. package/templates/web-app/src/finebi/client.ts +20 -4
  123. package/templates/web-app/src/finebi/config.d.ts +3 -1
  124. package/templates/web-app/src/finebi/config.js +58 -44
  125. package/templates/web-app/src/finebi/date.d.ts +1 -1
  126. package/templates/web-app/src/finebi/date.js +5 -5
  127. package/templates/web-app/src/finebi/filters.d.ts +4 -4
  128. package/templates/web-app/src/finebi/filters.js +106 -106
  129. package/templates/web-app/src/finebi/normalize.ts +235 -77
  130. package/templates/web-app/src/finebi/types.ts +20 -18
  131. package/templates/web-app/src/main.tsx +5 -5
  132. package/templates/web-app/src/retail.css +12 -12
  133. package/templates/web-app/src/services/presentation.ts +36 -36
  134. package/templates/web-app/src/services/table-data.ts +47 -47
  135. package/templates/web-app/src/services/view-data.ts +18 -18
  136. package/templates/web-app/src/services/widget-queries.ts +18 -18
  137. package/templates/web-app/tests/browser/fixtures.mjs +28 -28
  138. package/templates/web-app/tests/browser/index.html +16 -16
  139. package/templates/web-app/tests/browser/main.tsx +149 -149
  140. package/templates/web-app/tests/browser/test-page.css +4 -4
  141. package/templates/web-app/tests/browser/vite.config.mjs +13 -13
  142. package/templates/web-app/tests/bundle.test.mjs +19 -19
  143. package/templates/web-app/tests/cockpit.test.mjs +73 -73
  144. package/templates/web-app/tests/field-selection.test.mjs +70 -0
  145. package/templates/web-app/tests/proxy.test.mjs +20 -5
  146. package/templates/web-app/tests/retail.test.mjs +31 -31
  147. package/templates/web-app/tests/runtime.test.mjs +7 -0
  148. package/templates/web-app/tests/session.test.mjs +91 -0
  149. package/templates/web-app/tests/table-normalize.test.mjs +189 -0
  150. package/templates/web-app/tests/vite-auth.test.mjs +41 -0
  151. package/templates/web-app/tsconfig.json +4 -4
  152. package/templates/web-app/vite.config.ts +9 -4
@@ -0,0 +1,189 @@
1
+ import assert from 'node:assert/strict';
2
+ import {after, test} from 'node:test';
3
+ import {createServer} from 'vite';
4
+ import {createElement} from 'react';
5
+ import {renderToStaticMarkup} from 'react-dom/server';
6
+ const server = await createServer({configFile: false, cacheDir: '../../../temp/vite-table-tests', server: {middlewareMode: true, hmr: false}});
7
+ after(() => server.close());
8
+ const {normalize} = await server.ssrLoadModule('/src/finebi/normalize.ts');
9
+ const {DataTable} = await server.ssrLoadModule('/src/components/DataTable.tsx');
10
+ const {tableCsv} = await server.ssrLoadModule('/src/services/table-data.ts');
11
+ const metric = (text, rowSumInfo = {totalSum: false, innerSum: false}, extra = {}) => ({dId: 'amount', text, authority: true, rowSumInfo, ...extra});
12
+ const group = items => ({previewResult: {resultType: 'BIGROUP', header: [{dId: 'region', text: '地区'}, {dId: 'city', text: '城市'}, {dId: 'amount', text: '金额'}], items, hasNext: true, hasPrev: false}, widgetBean: {dimensions: {region: {type: 1}, city: {type: 1}}, settings: {tableStyle: {rowCounts: 10}}}});
13
+ const detailInfo = {totalSum: false, innerSum: false};
14
+ const totalInfo = {totalSum: true, innerSum: false};
15
+ const node = (dId, text, children = null, values = null) => ({dId, text, formatText: text, authority: true, children, values});
16
+ const crossMetric = (text, colSumInfo = detailInfo, rowSumInfo = detailInfo, dId = 'amount') => ({dId, text, authority: true, colSumInfo, rowSumInfo});
17
+ const cross = (top, left) => ({previewResult: {resultType: 'BICROSS', top: {header: [{dId: 'year', text: '年'}], items: [node(null, null, top)]}, left: {header: [{dId: 'region', text: '地区'}], items: [node(null, null, left)]}, hasXNext: true, hasXPrev: false, hasYNext: false, hasYPrev: false}});
18
+
19
+ test('cross display removes only repeated metric captions and DATA summary carriers', () => {
20
+ const source = cross([node('region', '金额', [node('target-name', '金额', null, [node('amount', '金额')])])], [
21
+ node('region', '合计', [node('__summary_dim_id__', '合计', null, [crossMetric('1')])]),
22
+ node('__summary_dim_id__', '合计', null, [crossMetric('2', detailInfo, totalInfo)]),
23
+ node('__summary_dim_id__', '合计', null, [crossMetric('3', detailInfo, {totalSum: false, innerSum: true})]),
24
+ node('__summary_dim_id__', '合计', null, [{...crossMetric('4'), rowSumInfo: null}]),
25
+ ]);
26
+ const data = normalize(source).datasets[0];
27
+ assert.equal(data.columns[1].name, '金额 / 金额', 'real repeated grouping labels must remain');
28
+ assert.equal(data.columns[1].path.length, 3, 'source path metadata must remain intact');
29
+ assert.equal(data.rows[0].cells[0].display, '合计', 'business dimension named 合计 must remain');
30
+ assert.equal(data.rows[0].path.length, 2);
31
+ assert.deepEqual(data.rows.slice(1).map(row => [row.kind, row.cells[0].display]), [['GRAND_TOTAL', '合计'], ['SUBTOTAL', '合计'], ['UNKNOWN', '合计']]);
32
+ });
33
+
34
+ test('cross metric rows deduplicate only the verified trailing metric caption', () => {
35
+ const rows = [
36
+ node('region', '金额', [node('target-name', '金额', [node('amount', '金额', null, [crossMetric('7')])])]),
37
+ node('region', '金额', [node('other-dimension', '金额', null, [crossMetric('8')])]),
38
+ node('target-name', '金额', [node('amount', '别名', null, [crossMetric('9')])]),
39
+ node('target-name', '金额', [node('amount', '金额', null, [crossMetric('10', detailInfo, detailInfo, 'count')])]),
40
+ ];
41
+ const data = normalize(cross([node('year', '2024', null, [{}])], rows)).datasets[0];
42
+ assert.deepEqual(data.rows.map(r => r.cells[0].display), ['金额 / 金额', '金额 / 金额', '金额 / 别名', '金额 / 金额']);
43
+ assert.equal(data.rows[0].path.length, 3);
44
+ assert.equal(data.rows[0].cells[1].sourceFieldId, 'amount');
45
+ const html = renderToStaticMarkup(createElement(DataTable, {dataset: data, pageSize: 10, binding: {type: 'table'}}));
46
+ assert.doesNotMatch(html, /金额 \/ 金额 \/ 金额/);
47
+ assert.doesNotMatch(tableCsv(data, data.rows), /金额 \/ 金额 \/ 金额/);
48
+ });
49
+
50
+ test('cross real null renders as dash while placeholders, missing payload and permissions stay distinct', () => {
51
+ const empty = {...crossMetric(null), value: null, formatText: null};
52
+ const cells = [empty, {...empty, dId: null}, {...empty, authority: false}, {...empty, authority: undefined}, {dId: 'amount', authority: true}, {...empty, formatText: '仅展示'}];
53
+ const data = normalize(cross([node('year', '2024', null, cells.map(() => ({})))], [node('region', '东部', null, cells)])).datasets[0];
54
+ assert.deepEqual(data.rows[0].cells.slice(1).map(c => c.state), ['NULL', 'UNAVAILABLE', 'RESTRICTED', 'UNAVAILABLE', 'UNAVAILABLE', 'UNAVAILABLE']);
55
+ assert.equal(data.rows[0].cells[1].reason, null);
56
+ const html = renderToStaticMarkup(createElement(DataTable, {dataset: data, pageSize: 10, binding: {type: 'table'}}));
57
+ assert.match(html, /<td>—<\/td><td>不可用<\/td><td>受限<\/td>/);
58
+ assert.match(tableCsv(data, data.rows), /"—","不可用","受限"/);
59
+ });
60
+
61
+ test('cross placeholders without valid metric ids never become available numeric values', () => {
62
+ const cells = [null, undefined, '', ' ', 12].map(dId => ({...crossMetric('12'), dId, value: 12}));
63
+ const data = normalize(cross([node('year', '2024', null, cells.map(() => ({})))], [node('region', '东部', null, cells)])).datasets[0];
64
+ for (const cell of data.rows[0].cells.slice(1)) {
65
+ assert.equal(cell.state, 'UNAVAILABLE');
66
+ assert.equal(cell.value, null);
67
+ assert.equal(cell.sourceFieldId, null);
68
+ }
69
+ });
70
+
71
+ test('cross table aligns repeated metric ids by column slots and preserves both axes and totals', () => {
72
+ for (const totalFirst of [false, true]) {
73
+ const columns = [node('year', '2024', null, [node('amount', '金额')]), node('year', '2025', null, [node('amount', '金额')])];
74
+ const summary = node('__summary_dim_id__', '合计', [node('amount', '金额', null, [{}])]);
75
+ columns.splice(totalFirst ? 0 : columns.length, 0, summary);
76
+ const values = [crossMetric('9007199254740993.12'), crossMetric('2.50')];
77
+ values.splice(totalFirst ? 0 : values.length, 0, crossMetric('9007199254740995.62', totalInfo));
78
+ const source = cross(columns, [node('region', '东部', [node('__summary_dim_id__', null, null, values)]), node('__summary_dim_id__', '合计', [node('amount', '金额', null, values.map(v => ({...v, rowSumInfo: totalInfo})))])]);
79
+ const result = normalize(source), data = result.datasets[0];
80
+ assert.equal(result.resultType, 'CROSS_TABLE'); assert.equal(result.normalization, 'COMPLETE');
81
+ assert.deepEqual(data.rows[0].cells.slice(1).map(c => c.value), values.map(v => v.text));
82
+ assert.equal(new Set(data.columns.map(c => c.fieldId)).size, 4);
83
+ assert.equal(data.rows[0].cells[1].sourceFieldId, 'amount');
84
+ assert.deepEqual(data.rows.map(r => r.kind), ['DATA', 'GRAND_TOTAL']);
85
+ assert.equal(data.columns[totalFirst ? 1 : 3].kind, 'GRAND_TOTAL');
86
+ assert.equal(result.range.mode, 'INITIAL_VIEW'); assert.equal(result.range.page, null);
87
+ assert.deepEqual(result.range.axes, {horizontal: {hasNext: true, hasPrevious: false}, vertical: {hasNext: false, hasPrevious: false}});
88
+ assert.equal(result.range.hasNext, true); assert.ok(result.warnings.includes('NEXT_PAGE_UNSUPPORTED'));
89
+ const html = renderToStaticMarkup(createElement(DataTable, {dataset: data, pageSize: 10, binding: {type: 'table'}}));
90
+ assert.match(html, /2024/); assert.match(html, /2025/); assert.match(html, /总计/);
91
+ assert.match(tableCsv(data, data.rows), /9007199254740993.12/);
92
+ assert.equal(source.previewResult.top.items[0].children.length, 3);
93
+ }
94
+ });
95
+
96
+ test('cross table supports metrics on rows and multi-level columns without id joins', () => {
97
+ const source = cross([node('year', '2024', [node('quarter', 'Q1', null, []), node('quarter', 'Q2', null, [])])], [
98
+ node('region', '东部', [node('target-name', '金额', [node('amount', '金额', null, [crossMetric('12'), crossMetric('18')])]), node('target-name', '数量', [node('count', '数量', null, [crossMetric('3', detailInfo, detailInfo, 'count'), crossMetric('4', detailInfo, detailInfo, 'count')])])]),
99
+ ]);
100
+ const data = normalize(source).datasets[0];
101
+ assert.equal(data.columns.length, 3); assert.equal(data.rows.length, 2);
102
+ assert.match(data.columns[1].name, /2024.*Q1/);
103
+ assert.match(data.rows[1].cells[0].display, /东部.*数量/);
104
+ assert.deepEqual(data.rows[1].cells.slice(1).map(c => c.sourceFieldId), ['count', 'count']);
105
+ });
106
+
107
+ test('cross table never exposes restricted axis labels and retains missing/ambiguous value states', () => {
108
+ const secret = node('year', 'secret column', null, [{}]); secret.authority = false;
109
+ const row = node('region', 'secret row', null, [crossMetric('123')]); row.authority = false;
110
+ const result = normalize(cross([secret], [row]));
111
+ assert.doesNotMatch(JSON.stringify(result), /secret column|secret row/);
112
+ assert.equal(result.datasets[0].rows[0].cells[0].state, 'RESTRICTED');
113
+ assert.equal(result.datasets[0].rows[0].cells[1].state, 'RESTRICTED');
114
+ const source = cross([node('year', 'A', null, [{}, {}])], [node('region', 'X', null, [{...crossMetric('1万'), formatText: '1万'}])]);
115
+ const incomplete = normalize(source);
116
+ assert.equal(incomplete.normalization, 'PARTIAL');
117
+ assert.equal(incomplete.datasets[0].rows[0].cells[1].state, 'UNAVAILABLE');
118
+ assert.equal(incomplete.datasets[0].rows[0].cells[2].reason, 'MISSING_CELL');
119
+ source.previewResult.left.items[0].children[0].values.push(crossMetric('2'), crossMetric('3'));
120
+ assert.equal(normalize(source).normalization, 'UNSUPPORTED', 'extra values must not be silently dropped');
121
+ });
122
+
123
+ test('cross table distinguishes empty views, missing axes and unsupported mixed node topology', () => {
124
+ assert.equal(normalize(cross([], [])).normalization, 'COMPLETE');
125
+ assert.equal(normalize(cross([], [])).datasets[0].returnedRowCount, 0);
126
+ assert.equal(normalize({previewResult: {resultType: 'BICROSS', left: {header: [], items: []}}}).normalization, 'UNSUPPORTED');
127
+ assert.equal(normalize(cross([node('x', 'A', [node('x', 'B')], [{}])], [])).normalization, 'UNSUPPORTED');
128
+ });
129
+
130
+ test('cross table handles no column dimensions and preserves metric-name intersection placeholders', () => {
131
+ const source = cross([], [node('target', '金额', null, [crossMetric('7'), {dId: null, text: null, value: null, authority: true}])]);
132
+ source.previewResult.top = {header: [], items: []};
133
+ const result = normalize(source);
134
+ assert.equal(result.normalization, 'COMPLETE');
135
+ assert.deepEqual(result.datasets[0].rows[0].cells.slice(1).map(c => c.state), ['AVAILABLE', 'UNAVAILABLE']);
136
+ assert.equal(result.datasets[0].rows[0].cells[2].value, null);
137
+ const swapped = cross([node('year', '2024', null, [node('amount', '金额')])], [node('region', '东部', null, [crossMetric('1', detailInfo, detailInfo, 'count')])]);
138
+ assert.equal(normalize(swapped).normalization, 'PARTIAL');
139
+ assert.equal(normalize(swapped).datasets[0].rows[0].cells[1].reason, 'AMBIGUOUS_FIELD');
140
+ });
141
+
142
+ test('group preserves parent keys, totals, metric precision and the first group page', () => {
143
+ const source = group([{dId: 'region', text: '东部', authority: true, values: [metric('9007199254740993.12', {totalSum: false, innerSum: true})], children: [
144
+ {dId: 'city', text: '甲城', authority: true, values: [metric('12.50', undefined, {formatText: '12.50元'})]},
145
+ ]}, {values: [metric('9007199254741005.62', {totalSum: true, innerSum: false})]}]);
146
+ const result = normalize(source), data = result.datasets[0];
147
+ assert.equal(result.resultType, 'GROUP_TABLE');
148
+ assert.equal(result.normalization, 'COMPLETE');
149
+ assert.deepEqual(data.rows.map(r => r.kind), ['SUBTOTAL', 'DATA', 'GRAND_TOTAL']);
150
+ assert.deepEqual(data.rows[1].cells.map(c => c.value), ['东部', '甲城', '12.50']);
151
+ assert.equal(data.rows[0].cells[2].value, '9007199254740993.12');
152
+ assert.equal(data.returnedRowCount, 3);
153
+ assert.deepEqual(result.range, {mode: 'PAGE', page: 1, pageSize: 10, pageUnit: 'GROUP', hasNext: true, hasPrevious: false, total: {value: null, unit: 'GROUP'}});
154
+ assert.ok(result.warnings.includes('NEXT_PAGE_UNSUPPORTED'));
155
+ const html = renderToStaticMarkup(createElement(DataTable, {dataset: data, pageSize: 10, binding: {type: 'table'}}));
156
+ assert.match(html, /小计/); assert.match(html, /总计/);
157
+ assert.match(tableCsv(data, data.rows), /GRAND_TOTAL/);
158
+ });
159
+
160
+ test('group keeps restricted and ambiguous cells unavailable and does not infer numeric group keys', () => {
161
+ const source = group([{dId: 'region', text: '42', authority: true, values: [metric('12万', undefined, {formatText: '12万'})], children: [
162
+ {dId: 'city', text: '秘密', authority: false, values: [metric('999', undefined, {authority: false})]},
163
+ {dId: 'city', text: '乙城', authority: true, values: [metric('1'), metric('2')]},
164
+ {dId: 'city', text: '丙城', authority: true, values: [metric(null, null)]},
165
+ ]}]);
166
+ source.widgetBean.dimensions.region.type = 2;
167
+ const result = normalize(source), rows = result.datasets[0].rows;
168
+ assert.equal(rows[0].cells[0].reason, 'GROUP_KEY_SEMANTICS_UNKNOWN');
169
+ assert.equal(rows[0].cells[2].state, 'UNAVAILABLE');
170
+ assert.equal(rows[1].cells[1].state, 'RESTRICTED');
171
+ assert.equal(rows[1].cells[2].state, 'RESTRICTED');
172
+ assert.equal(rows[2].cells[2].reason, 'AMBIGUOUS_FIELD');
173
+ assert.equal(rows[3].cells[2].state, 'UNAVAILABLE');
174
+ assert.equal(rows[3].kind, 'UNKNOWN');
175
+ assert.equal(result.normalization, 'PARTIAL');
176
+ });
177
+
178
+ test('group header expansion reports an initial view and malformed groups are not empty successes', () => {
179
+ const source = group([]);
180
+ source.widgetBean.settings.tableStyle.showRowHeaderIcon = true;
181
+ const result = normalize(source);
182
+ assert.equal(result.range.mode, 'INITIAL_VIEW');
183
+ assert.equal(result.range.page, null);
184
+ assert.equal(result.range.hasNext, null);
185
+ assert.equal(result.datasets[0].returnedRowCount, 0);
186
+ assert.equal(normalize({previewResult: {resultType: 'BIGROUP'}}).normalization, 'UNSUPPORTED');
187
+ source.previewResult.items = [null, {dId: 'unknown', values: [metric('1')]}];
188
+ assert.equal(normalize(source).normalization, 'PARTIAL');
189
+ });
@@ -0,0 +1,41 @@
1
+ import assert from 'node:assert/strict';
2
+ import {test} from 'node:test';
3
+ import {createServer as httpServer} from 'node:http';
4
+ import {mkdir,mkdtemp,cp,writeFile,readFile} from 'node:fs/promises';
5
+ import {resolve} from 'node:path';
6
+ import {createServer} from 'vite';
7
+
8
+ test('real Vite uses project env, persists sessions, blocks credential files and survives token updates',async()=>{
9
+ await mkdir('temp',{recursive:true});
10
+ const root=await mkdtemp(resolve('temp','vite-auth-'));
11
+ for(const name of ['vite.config.ts','scripts','src','public','index.html','package.json'])await cp(resolve(name),resolve(root,name),{recursive:true});
12
+ await mkdir(resolve(root,'temp'));
13
+ await writeFile(resolve(root,'temp','staged-session'),'staged-secret');
14
+ let logins=0,queries=0;
15
+ const upstream=httpServer(async(req,res)=>{
16
+ for await(const _ of req){};
17
+ res.setHeader('content-type','application/json');
18
+ if(req.url==='/decision/login'){logins++;res.end(JSON.stringify({data:{accessToken:'persisted-session'}}));return;}
19
+ assert.equal(req.headers.cookie,'fine_auth_token=persisted-session');queries++;
20
+ res.end(JSON.stringify({success:true,data:{}}));
21
+ });
22
+ await new Promise(r=>upstream.listen(0,'127.0.0.1',r));
23
+ const baseUrl=`http://127.0.0.1:${upstream.address().port}/decision`;
24
+ await writeFile(resolve(root,'.env.local'),`FINEBI_DEV_BASE_URL=${baseUrl}\nFINEBI_DEV_USERNAME=alice\nFINEBI_DEV_PASSWORD=password\n`);
25
+ await writeFile(resolve(root,'public','app-config.json'),JSON.stringify({basePath:'/decision',dashboardId:'r',widgets:[{entryId:'w'}],maxRows:10,filters:[]}));
26
+ const server=await createServer({root,configFile:resolve(root,'vite.config.ts'),cacheDir:resolve(root,'temp','cache'),server:{port:0},logLevel:'silent'});
27
+ try {
28
+ await server.listen();
29
+ const url=`http://127.0.0.1:${server.httpServer.address().port}`;
30
+ assert.equal((await fetch(url+'/')).status,200,'project under a temp parent must still serve its index');
31
+ assert.equal((await fetch(url+'/src/main.tsx')).status,200,'project source must remain loadable');
32
+ const send=()=>fetch(url+'/decision/v5/api/dashboard/report/r/widgets/query',{method:'POST',headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'},body:JSON.stringify({widgetIds:['w'],pageLimit:10})});
33
+ const result=await send();assert.equal(result.status,200);assert.doesNotMatch(await result.text(),/persisted-session|password/);
34
+ assert.match(await readFile(resolve(root,'.env.local'),'utf8'),/FINEBI_DEV_TOKEN="persisted-session"/);
35
+ assert.equal((await fetch(url+'/.env.local')).status,403);
36
+ assert.equal((await fetch(url+'/temp/staged-session')).status,403);
37
+ // Allow the file watcher to process the save; it must not restart the session manager.
38
+ await new Promise(r=>setTimeout(r,400));
39
+ assert.equal((await send()).status,200);assert.equal(logins,1);assert.equal(queries,2);
40
+ } finally {await server.close();await new Promise(r=>upstream.close(r));}
41
+ });
@@ -1,4 +1,4 @@
1
- {
2
- "compilerOptions": {"target":"ES2020","lib":["ES2020","DOM","DOM.Iterable"],"module":"ESNext","moduleResolution":"Bundler","jsx":"react-jsx","strict":true,"skipLibCheck":true,"esModuleInterop":true,"resolveJsonModule":true,"isolatedModules":true,"noEmit":true},
3
- "include":["src","vite.config.ts"]
4
- }
1
+ {
2
+ "compilerOptions": {"target":"ES2020","lib":["ES2020","DOM","DOM.Iterable"],"module":"ESNext","moduleResolution":"Bundler","jsx":"react-jsx","strict":true,"skipLibCheck":true,"esModuleInterop":true,"resolveJsonModule":true,"isolatedModules":true,"noEmit":true},
3
+ "include":["src","vite.config.ts"]
4
+ }
@@ -1,15 +1,20 @@
1
1
  import {readFileSync} from 'node:fs';
2
+ import {resolve} from 'node:path';
2
3
  import {fileURLToPath} from 'node:url';
3
- import {defineConfig, loadEnv} from 'vite';
4
+ import {defineConfig, loadEnv, normalizePath} from 'vite';
4
5
  // @ts-expect-error The development-only Node module is tested directly with node:test.
5
6
  import {createDevMiddleware} from './scripts/dev-proxy.mjs';
7
+ const projectTemp=normalizePath(fileURLToPath(new URL('./temp',import.meta.url)))+'/**';
6
8
  export default defineConfig(({mode}) => ({
7
9
  base: './',
8
10
  resolve:{alias:[{find:/^date-fns\/locale$/,replacement:fileURLToPath(new URL('./src/components/calendar-locales.ts',import.meta.url))}]},
9
- server: {host:'127.0.0.1',port:4173,strictPort:true},
11
+ server: {host:'127.0.0.1',port:4173,strictPort:true,watch:{ignored:['**/.env.local',projectTemp]},
12
+ fs:{deny:['.env','.env.*','*.{crt,pem}','**/.git/**',projectTemp]}},
10
13
  plugins:[{name:'finebi-development-session',apply:'serve',configureServer(server) {
11
14
  const config=JSON.parse(readFileSync(new URL('./public/app-config.json',import.meta.url),'utf8'));
12
- const env={...loadEnv(mode,process.cwd(),'FINEBI_DEV_'),...process.env};
13
- server.middlewares.use(createDevMiddleware(config,env));
15
+ const env={...loadEnv(mode,server.config.root,'FINEBI_DEV_'),...process.env};
16
+ const middleware=createDevMiddleware(config,env,fetch,{file:resolve(server.config.root,'.env.local')});
17
+ server.middlewares.use(middleware);
18
+ server.httpServer?.once('close',()=>middleware.dispose());
14
19
  }}],
15
20
  }));