Skip to content

Commit f2147ce

Browse files
paddymulclaude
andauthored
test: assert histogram rendering in Playwright integration tests (#631)
* test: assert histogram DOM elements render in marimo and Jupyter integration tests The existing Playwright tests exercise the full Python→serialize→JS→render pipeline but only assert on data grid cells, row counts, and headers. They never check that summary stats (histograms) actually render, which allowed the parquet_b64 async regression (#630) to ship undetected. Add .histogram-component visibility assertions to both marimo.spec.ts and integration-batch.spec.ts. These will fail until #630 is fixed, proving the test gap. Refs: #630 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: cap fastparquet <2026.3.0 (missing x86_64 Linux wheels) fastparquet 2026.3.0 (released 2026-03-17) ships without manylinux x86_64 wheels or sdist, breaking installation on GitHub Actions Ubuntu runners. Cap to <2026.3.0 until a fixed release is available. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: generate bigint-test.html for static embed Playwright test The BigInt precision test (added in 44a2af1) navigates to /bigint-test.html but the generation script only created static-test.html. The test always timed out at 60s waiting for AG-Grid cells on a nonexistent page. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: bigint test DataFrame needs enough rows for widget initialization A 3-row DataFrame with only bigint values causes histogram computation to fail ("Too many bins for data range"), leaving df_data_dict with only ['empty'] and no 'all_stats' key. to_html() then hits a KeyError. Pad to 10 rows with a normal 'value' column so the widget fully initializes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: handle np.histogram ValueError for large integers near 2^53 np.histogram(meat, 10) raises ValueError when float64 precision is insufficient to create distinct bin edges. At values near 2^53, np.spacing is 2.0, so the ±0.5 range numpy uses for single-value binning collapses to zero width. This affects any column with large integers (database PKs, snowflake IDs) where percentile trimming leaves few values. Catch the ValueError and return empty histogram_args, which falls through to categorical histogram display. Also reverts the bigint test DataFrame padding since the 3-row fixture now works correctly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add unit tests for parquet_b64 summary stats round-trip (#630) Python tests (test_sd_to_parquet_b64.py): - sd_to_parquet_b64 returns tagged dict with correct format - Scalar values round-trip through parquet encoding - Histogram arrays survive encoding with correct types (numbers not strings) - Categorical histogram data round-trips correctly - Multiple columns are handled JS tests (resolveDFData.test.ts): - Null/undefined/plain-array pass-through - hyparquet can decode Python-generated parquet_b64 fixture - Histogram JSON round-trips with correct types after parquet decode - resolveDFDataAsync returns non-empty decoded data - **resolveDFData (sync) returns non-empty data** — this test FAILS, proving #630: parquetRead is async so the sync wrapper returns [] Jest config: add hyparquet ESM module resolution + TextDecoder polyfill. Refs: #630 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: revert summary stats to JSON serialization (#630) The parquet_b64 transport for summary stats (PR #488) broke histograms and all summary stats in both Jupyter and marimo. hyparquet's parquetRead is async, so the synchronous resolveDFData() returned [] before the decode callback fired. Revert all widget _sd_to_jsondf() methods and _summary_to_rows() back to pd_to_obj(pd.DataFrame(sd)) which produces plain JSON arrays. The JS resolveDFData() passes arrays through unchanged (Array.isArray path). The static embed path (artifact.py → BuckarooStaticTable.tsx) is unaffected — it uses resolveDFDataAsync() which properly awaits. Summary stats DataFrames are small, so JSON transport has negligible performance impact vs parquet. Fixes: #630 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Revert "fix: revert summary stats to JSON serialization (#630)" This reverts commit cdce878. * fix: use async parquet decode for summary stats in widget components (#630) The sync resolveDFData() returns [] because hyparquet's parquetRead is async. Instead of reverting to JSON, add a useResolvedDFDataDict React hook that: 1. Tries sync decode first (works for plain arrays and cached data) 2. Falls back to async resolveDFDataAsync if parquet_b64 returned [] 3. Re-renders when async decode completes Updated all widget entry points: - DCFCell: useResolvedDFDataDict instead of resolveDFData - BuckarooInfiniteWidget: same - DFViewerInfiniteDS: same - widget.tsx renderDFV: same getDataWrapper now takes a pre-resolved dict instead of calling resolveDFData internally. Also fixes Python lint (unused imports in test_sd_to_parquet_b64.py). Fixes: #630 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: drop ?? [] fallbacks — missing keys should surface as errors The resolved dict always has every key from df_data_dict. A missing key means a real bug (wrong data_key config, widget state mismatch) that should be visible, not silently masked with an empty array. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: pre-decode parquet_b64 at widget mount, not in React components Replace the useResolvedDFDataDict hook with a simpler approach: createPredecodingRender() decodes all parquet_b64 entries in df_data_dict ONCE via preResolveDFDataDict() before React mounts, and re-decodes on model changes. React components now receive plain DFData arrays and never deal with parquet_b64 — no hooks, no async, no empty-frame flash. - widget.tsx: new createPredecodingRender() that awaits decode before React.createElement, used by BuckarooWidget, BuckarooInfiniteWidget, and DFViewerInfiniteDS render paths - DCFCell, BuckarooWidgetInfinite: revert to plain df_data_dict[key] access (no resolveDFData, no hook) - Remove useResolvedDFDataDict.ts — no longer needed Fixes: #630 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: move histogram assertion to integration.spec.ts (the one CI runs) Codex correctly noted that CI runs integration.spec.ts per-notebook, not integration-batch.spec.ts. Move the histogram assertion to the file that actually executes in the Jupyter Playwright job. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0bf6253 commit f2147ce

14 files changed

Lines changed: 346 additions & 34 deletions

File tree

buckaroo/customizations/histogram.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,14 @@ def series_summary(sampled_ser, ser):
130130
meat = vals[low_pass & high_pass]
131131
if len(meat) == 0:
132132
return dict(histogram_args={})
133-
134-
meat_histogram=np.histogram(meat, 10)
133+
134+
try:
135+
meat_histogram=np.histogram(meat, 10)
136+
except ValueError:
137+
# Can happen when float64 precision is insufficient to create
138+
# 10 distinct bin edges (e.g. large integers near 2^53 where
139+
# np.spacing > bin width).
140+
return dict(histogram_args={})
135141
populations, _ = meat_histogram
136142
return dict(
137143
histogram_bins = meat_histogram[1],

packages/buckaroo-js-core/jest.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ export default {
33
transform: {
44
"^.+\\.tsx?$": "ts-jest",
55
"lodash-es.+\\.js$": "ts-jest",
6+
"hyparquet.+\\.js$": "ts-jest",
67
},
7-
transformIgnorePatterns: ["node_modules/(?!.*lodash-es)"],
8+
transformIgnorePatterns: ["node_modules/(?!.*(lodash-es|hyparquet))"],
89

910
moduleNameMapper: {
1011
"\\.(css|less|sass|scss)$": "identity-obj-proxy",
1112
"^.+\\.svg$": "jest-transformer-svg",
1213
"^@/(.*)$": "<rootDir>/src/$1",
14+
"^hyparquet$": "<rootDir>/node_modules/hyparquet/src/index.js",
1315
},
1416

1517
testMatch: ["!**/*.spec.ts", "**/*.test.ts", "**/*.test.tsx"],
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,10 @@
11
import "@testing-library/jest-dom";
2+
import { TextDecoder, TextEncoder } from 'util';
3+
4+
// hyparquet uses TextDecoder/TextEncoder which jsdom doesn't provide
5+
if (typeof globalThis.TextDecoder === 'undefined') {
6+
(globalThis as any).TextDecoder = TextDecoder;
7+
}
8+
if (typeof globalThis.TextEncoder === 'undefined') {
9+
(globalThis as any).TextEncoder = TextEncoder;
10+
}

packages/buckaroo-js-core/pw-tests/integration.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,13 @@ test.describe('Buckaroo Widget JupyterLab Integration', () => {
186186
await expect(ageCell).toBeVisible();
187187
await expect(scoreCell).toBeVisible();
188188

189+
// Summary stats histograms should render in pinned rows.
190+
// The test notebook has numeric columns (age, score) that produce histogram bars.
191+
const histograms = page.locator('.histogram-component');
192+
await expect(histograms.first()).toBeVisible({ timeout: 10_000 });
193+
expect(await histograms.count()).toBeGreaterThan(0);
194+
console.log(`✅ Found ${await histograms.count()} histogram(s)`);
195+
189196
console.log(`🎉 SUCCESS: Widget from ${notebookName} rendered ag-grid with ${rowCount} rows, ${headerCount} columns, and ${cellCount} cells`);
190197
console.log('📊 Verified data: Alice (age 25, score 85.5)');
191198
});

packages/buckaroo-js-core/pw-tests/marimo.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,20 @@ test.describe('Buckaroo in marimo', () => {
118118
expect(await getCellText(secondWidget, 'b', 0)).toBe('0');
119119
expect(await getCellText(secondWidget, 'c', 0)).toBe('row_0');
120120
});
121+
122+
test('summary stats histograms render', async ({ page }) => {
123+
await page.goto('/');
124+
await waitForGrid(page);
125+
126+
// The large DataFrame (200 rows, numeric columns) should produce histograms.
127+
// Histograms are rendered as pinned summary rows with .histogram-component divs
128+
// containing Recharts BarChart SVGs.
129+
const widgets = page.locator('.buckaroo_anywidget');
130+
await widgets.nth(1).locator('.ag-cell').first().waitFor({ state: 'visible', timeout: 30_000 });
131+
132+
const secondWidget = widgets.nth(1);
133+
const histograms = secondWidget.locator('.histogram-component');
134+
await expect(histograms.first()).toBeVisible({ timeout: 10_000 });
135+
expect(await histograms.count()).toBeGreaterThan(0);
136+
});
121137
});

packages/buckaroo-js-core/src/components/BuckarooWidgetInfinite.tsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import * as _ from "lodash-es";
33
import { OperationResult } from "./DependentTabs";
44
import { ColumnsEditor } from "./ColumnsEditor";
55

6-
import { DFData, DFDataOrPayload } from "./DFViewerParts/DFWhole";
7-
import { resolveDFData } from "./DFViewerParts/resolveDFData";
6+
import { DFData } from "./DFViewerParts/DFWhole";
87
import { StatusBar } from "./StatusBar";
98
import { BuckarooState } from "./WidgetTypes";
109
import { BuckarooOptions } from "./WidgetTypes";
@@ -23,7 +22,7 @@ import { MessageBox } from "./MessageBox";
2322

2423
export const getDataWrapper = (
2524
data_key: string,
26-
df_data_dict: Record<string, DFDataOrPayload>,
25+
df_data_dict: Record<string, DFData>,
2726
ds: IDatasource,
2827
total_rows?: number
2928
): DatasourceOrRaw => {
@@ -34,11 +33,11 @@ export const getDataWrapper = (
3433
length: total_rows || 50,
3534
};
3635
} else {
37-
const resolved = resolveDFData(df_data_dict[data_key]);
36+
const data = df_data_dict[data_key];
3837
return {
3938
data_type: "Raw",
40-
data: resolved,
41-
length: resolved.length,
39+
data: data,
40+
length: data.length,
4241
};
4342
}
4443
};
@@ -123,7 +122,7 @@ export function BuckarooInfiniteWidget({
123122
src
124123
}: {
125124
df_meta: DFMeta;
126-
df_data_dict: Record<string, DFDataOrPayload>;
125+
df_data_dict: Record<string, DFData>;
127126
df_display_args: Record<string, IDisplayArgs>;
128127
operations: Operation[];
129128
on_operations: (ops: Operation[]) => void;
@@ -160,9 +159,9 @@ export function BuckarooInfiniteWidget({
160159
const [data_wrapper, summaryStatsData] = useMemo(
161160
() => [
162161
getDataWrapper(cDisp.data_key, df_data_dict, mainDs, df_meta.total_rows),
163-
resolveDFData(df_data_dict[cDisp.summary_stats_key]),
162+
df_data_dict[cDisp.summary_stats_key],
164163
],
165-
[cDisp, operations, buckaroo_state],
164+
[cDisp, operations, buckaroo_state, df_data_dict],
166165
);
167166

168167
//used to denote "this dataframe has been transformed", This is
@@ -220,7 +219,7 @@ export function DFViewerInfiniteDS({
220219
show_message_box
221220
}: {
222221
df_meta: DFMeta;
223-
df_data_dict: Record<string, DFDataOrPayload>;
222+
df_data_dict: Record<string, DFData>;
224223
df_display_args: Record<string, IDisplayArgs>;
225224
src: KeyAwareSmartRowCache,
226225
df_id: string // the memory id
@@ -251,7 +250,7 @@ export function DFViewerInfiniteDS({
251250
const [data_wrapper, summaryStatsData] = useMemo(
252251
() => [
253252
getDataWrapper(cDisp.data_key, df_data_dict, mainDs, df_meta.total_rows),
254-
resolveDFData(df_data_dict[cDisp.summary_stats_key]),
253+
df_data_dict[cDisp.summary_stats_key],
255254
],
256255
[cDisp, df_data_dict, mainDs, df_meta.total_rows]
257256
);

packages/buckaroo-js-core/src/components/DCFCell.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import * as _ from "lodash-es";
33
import { OperationResult } from "./DependentTabs";
44
import { ColumnsEditor } from "./ColumnsEditor";
55

6-
import { DFDataOrPayload } from "./DFViewerParts/DFWhole";
7-
import { resolveDFData } from "./DFViewerParts/resolveDFData";
6+
import { DFData } from "./DFViewerParts/DFWhole";
87
import { DFViewer } from "./DFViewerParts/DFViewerInfinite";
98
import { StatusBar } from "./StatusBar";
109
import { BuckarooState } from "./WidgetTypes";
@@ -27,7 +26,7 @@ export function WidgetDCFCell({
2726
buckaroo_options,
2827
}: {
2928
df_meta: DFMeta;
30-
df_data_dict: Record<string, DFDataOrPayload>;
29+
df_data_dict: Record<string, DFData>;
3130
df_display_args: Record<string, IDisplayArgs>;
3231
operations: Operation[];
3332
on_operations: (ops: Operation[]) => void;
@@ -45,8 +44,8 @@ export function WidgetDCFCell({
4544
} else {
4645
// console.log("cDisp", cDisp);
4746
}
48-
const dfData = resolveDFData(df_data_dict[cDisp.data_key]);
49-
const summaryStatsData = resolveDFData(df_data_dict[cDisp.summary_stats_key]);
47+
const dfData = df_data_dict[cDisp.data_key];
48+
const summaryStatsData = df_data_dict[cDisp.summary_stats_key];
5049

5150
return (
5251
<div className="dcf-root flex flex-col buckaroo-widget" style={{ width: "100%", height: "100%" }}>
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { parquetRead, parquetMetadata } from 'hyparquet';
2+
import { resolveDFData, resolveDFDataAsync } from './resolveDFData';
3+
import { DFData, DFDataRow, ParquetB64Payload } from './DFWhole';
4+
5+
// Fixture generated by Python's sd_to_parquet_b64() with a summary stats dict
6+
// containing numeric histogram data for one column.
7+
// eslint-disable-next-line @typescript-eslint/no-var-requires
8+
const fixture = require('./test-fixtures/summary_stats_parquet_b64.json');
9+
const parquetPayload: ParquetB64Payload = fixture as ParquetB64Payload;
10+
11+
function b64ToArrayBuffer(b64: string): ArrayBuffer {
12+
const bin = atob(b64);
13+
const bytes = new Uint8Array(bin.length);
14+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
15+
return bytes.buffer;
16+
}
17+
18+
describe('resolveDFData', () => {
19+
it('should pass through null/undefined as empty array', () => {
20+
expect(resolveDFData(null)).toEqual([]);
21+
expect(resolveDFData(undefined)).toEqual([]);
22+
});
23+
24+
it('should pass through plain DFData arrays unchanged', () => {
25+
const data: DFData = [
26+
{ index: 'mean', a: 50 },
27+
{ index: 'dtype', a: 'float64' },
28+
];
29+
expect(resolveDFData(data)).toBe(data);
30+
});
31+
32+
it('hyparquet can read the parquet_b64 fixture', async () => {
33+
// Verify the fixture is valid and hyparquet can decode it.
34+
// This is independent of resolveDFData — it tests the raw decode path.
35+
const buf = b64ToArrayBuffer(parquetPayload.data);
36+
const metadata = parquetMetadata(buf);
37+
expect(metadata.row_groups.length).toBeGreaterThan(0);
38+
39+
const rows: DFDataRow[] = [];
40+
await parquetRead({
41+
file: buf,
42+
metadata,
43+
rowFormat: 'object',
44+
onComplete: (data: any[]) => { rows.push(...data); },
45+
});
46+
47+
expect(rows.length).toBeGreaterThan(0);
48+
49+
// Should have an 'index' column with stat names
50+
const indices = rows.map(r => r.index).filter(Boolean);
51+
expect(indices).toContain('histogram');
52+
expect(indices).toContain('dtype');
53+
});
54+
55+
it('parquet_b64 histogram data round-trips with correct types', async () => {
56+
// Decode the fixture and verify histogram arrays have the right structure.
57+
const buf = b64ToArrayBuffer(parquetPayload.data);
58+
const metadata = parquetMetadata(buf);
59+
60+
const rows: DFDataRow[] = [];
61+
await parquetRead({
62+
file: buf,
63+
metadata,
64+
rowFormat: 'object',
65+
onComplete: (data: any[]) => { rows.push(...data); },
66+
});
67+
68+
const histRow = rows.find(r => r.index === 'histogram');
69+
expect(histRow).toBeDefined();
70+
71+
// Column 'a' contains the JSON-encoded histogram array
72+
const rawCell = histRow!['a'];
73+
expect(typeof rawCell).toBe('string');
74+
75+
const parsed = JSON.parse(rawCell as string);
76+
expect(Array.isArray(parsed)).toBe(true);
77+
expect(parsed.length).toBeGreaterThan(0);
78+
79+
// Verify types: population should be a number, not a string
80+
const popBar = parsed.find((b: any) => b.population !== undefined);
81+
expect(popBar).toBeDefined();
82+
expect(typeof popBar.population).toBe('number');
83+
expect(typeof parsed[0].name).toBe('string');
84+
});
85+
86+
it('sync resolveDFData returns [] for parquet_b64 (known async limitation)', () => {
87+
// Documents #630: parquetRead is async so the sync wrapper returns [].
88+
// Widget components use useResolvedDFDataDict which falls back to async.
89+
// The static embed path uses resolveDFDataAsync which works correctly.
90+
const result = resolveDFData(parquetPayload);
91+
expect(result.length).toBe(0);
92+
});
93+
94+
it('async resolveDFDataAsync returns non-empty result for parquet_b64', async () => {
95+
const result = await resolveDFDataAsync(parquetPayload);
96+
expect(result.length).toBeGreaterThan(0);
97+
98+
// Verify the histogram row was JSON-parsed correctly
99+
const histRow = result.find(r => r.index === 'histogram');
100+
expect(histRow).toBeDefined();
101+
expect(Array.isArray(histRow!['a'])).toBe(true);
102+
});
103+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"format": "parquet_b64", "data": "UEFSMRUEFewEFeoCTBUMFQASAAC2AlAJAAAAImZsb2F0NjQiBAAAAHRydWUBCLA1MC4wCQEAAFt7Im5hbWUiOiAiMC4wIC0gMS4wIiwgInRhaWwiOiAxfSwgeyIRIggxLTIFHURwb3B1bGF0aW9uIjogMTUuMH0uJgAMMjAtNEYnAAAyPicADDQwLTZGJwAEMzA6TgAMNjAtOEYnAAAyPicAEDgwLTk5Abk2nAA+JwAEOTkJ5QQwMDrnADBdAgAAAHt9AgAAAFtdFQAVFhUaLBUMFRAVBhUGHDYAKAJ7fRgJImZsb2F0NjQiAAAACygCAAAADAEDA4jGAhUEFaABFYIBTBUMFQASAABQsAUAAABkdHlwZQoAAABpc19udW1lcmljBAAAAG1lYW4JAAAAaGlzdG9ncmFtDi4NABRfYXJncw4yEgAMYmlucxUAFRYVGiwVDBUQFQYVBhw2ACgEbWVhbhgFZHR5cGUAAAALKAIAAAAMAQMDiMYCFQQVoAEVggFMFQwVABIAAFCwBQAAAGR0eXBlCgAAAGlzX251bWVyaWMEAAAAbWVhbgkAAABoaXN0b2dyYW0OLg0AFF9hcmdzDjISAAxiaW5zFQAVFhUaLBUMFRAVBhUGHDYAKARtZWFuGAVkdHlwZQAAAAsoAgAAAAwBAwOIxgIVBBWgARWCAUwVDBUAEgAAULAFAAAAZHR5cGUKAAAAaXNfbnVtZXJpYwQAAABtZWFuCQAAAGhpc3RvZ3JhbQ4uDQAUX2FyZ3MOMhIADGJpbnMVABUWFRosFQwVEBUGFQYcNgAoBG1lYW4YBWR0eXBlAAAACygCAAAADAEDA4jGAhUEGVw1ABgGc2NoZW1hFQgAFQwlAhgBYSUATBwAAAAVDCUCGAVpbmRleCUATBwAAAAVDCUCGAdsZXZlbF8wJQBMHAAAABUMJQIYEV9faW5kZXhfbGV2ZWxfMF9fJQBMHAAAABYMGRwZTCYAHBUMGTUABhAZGAFhFQIWDBbqBRbsAyaSAyYIHDYAKAJ7fRgJImZsb2F0NjQiABksFQQVABUCABUAFRAVAgA8FrwEGQYZJgAMAAAAJgAcFQwZNQAGEBkYBWluZGV4FQIWDBaaAhaAAiaWBSb0Axw2ACgEbWVhbhgFZHR5cGUAGSwVBBUAFQIAFQAVEBUCADwWcBkGGSYADAAAACYAHBUMGTUABhAZGAdsZXZlbF8wFQIWDBaaAhaAAiaWByb0BRw2ACgEbWVhbhgFZHR5cGUAGSwVBBUAFQIAFQAVEBUCADwWcBkGGSYADAAAACYAHBUMGTUABhAZGBFfX2luZGV4X2xldmVsXzBfXxUCFgwWmgIWgAImlgkm9AccNgAoBG1lYW4YBWR0eXBlABksFQQVABUCABUAFRAVAgA8FnAZBhkmAAwAAAAWuAwWDCYIFuwJABksGAZwYW5kYXMY0gV7ImluZGV4X2NvbHVtbnMiOiBbIl9faW5kZXhfbGV2ZWxfMF9fIl0sICJjb2x1bW5faW5kZXhlcyI6IFt7Im5hbWUiOiBudWxsLCAiZmllbGRfbmFtZSI6IG51bGwsICJwYW5kYXNfdHlwZSI6ICJ1bmljb2RlIiwgIm51bXB5X3R5cGUiOiAib2JqZWN0IiwgIm1ldGFkYXRhIjogeyJlbmNvZGluZyI6ICJVVEYtOCJ9fV0sICJjb2x1bW5zIjogW3sibmFtZSI6ICJhIiwgImZpZWxkX25hbWUiOiAiYSIsICJwYW5kYXNfdHlwZSI6ICJ1bmljb2RlIiwgIm51bXB5X3R5cGUiOiAib2JqZWN0IiwgIm1ldGFkYXRhIjogbnVsbH0sIHsibmFtZSI6ICJpbmRleCIsICJmaWVsZF9uYW1lIjogImluZGV4IiwgInBhbmRhc190eXBlIjogInVuaWNvZGUiLCAibnVtcHlfdHlwZSI6ICJvYmplY3QiLCAibWV0YWRhdGEiOiBudWxsfSwgeyJuYW1lIjogImxldmVsXzAiLCAiZmllbGRfbmFtZSI6ICJsZXZlbF8wIiwgInBhbmRhc190eXBlIjogInVuaWNvZGUiLCAibnVtcHlfdHlwZSI6ICJvYmplY3QiLCAibWV0YWRhdGEiOiBudWxsfSwgeyJuYW1lIjogbnVsbCwgImZpZWxkX25hbWUiOiAiX19pbmRleF9sZXZlbF8wX18iLCAicGFuZGFzX3R5cGUiOiAidW5pY29kZSIsICJudW1weV90eXBlIjogIm9iamVjdCIsICJtZXRhZGF0YSI6IG51bGx9XSwgImNyZWF0b3IiOiB7ImxpYnJhcnkiOiAicHlhcnJvdyIsICJ2ZXJzaW9uIjogIjIxLjAuMCJ9LCAicGFuZGFzX3ZlcnNpb24iOiAiMi4yLjMifQAYDEFSUk9XOnNjaGVtYRjsCi8vLy8vd2dFQUFBUUFBQUFBQUFLQUE0QUJnQUZBQWdBQ2dBQUFBQUJCQUFRQUFBQUFBQUtBQXdBQUFBRUFBZ0FDZ0FBQUFnREFBQUVBQUFBQVFBQUFBd0FBQUFJQUF3QUJBQUlBQWdBQUFEZ0FnQUFCQUFBQU5JQ0FBQjdJbWx1WkdWNFgyTnZiSFZ0Ym5NaU9pQmJJbDlmYVc1a1pYaGZiR1YyWld4Zk1GOWZJbDBzSUNKamIyeDFiVzVmYVc1a1pYaGxjeUk2SUZ0N0ltNWhiV1VpT2lCdWRXeHNMQ0FpWm1sbGJHUmZibUZ0WlNJNklHNTFiR3dzSUNKd1lXNWtZWE5mZEhsd1pTSTZJQ0oxYm1samIyUmxJaXdnSW01MWJYQjVYM1I1Y0dVaU9pQWliMkpxWldOMElpd2dJbTFsZEdGa1lYUmhJam9nZXlKbGJtTnZaR2x1WnlJNklDSlZWRVl0T0NKOWZWMHNJQ0pqYjJ4MWJXNXpJam9nVzNzaWJtRnRaU0k2SUNKaElpd2dJbVpwWld4a1gyNWhiV1VpT2lBaVlTSXNJQ0p3WVc1a1lYTmZkSGx3WlNJNklDSjFibWxqYjJSbElpd2dJbTUxYlhCNVgzUjVjR1VpT2lBaWIySnFaV04wSWl3Z0ltMWxkR0ZrWVhSaElqb2diblZzYkgwc0lIc2libUZ0WlNJNklDSnBibVJsZUNJc0lDSm1hV1ZzWkY5dVlXMWxJam9nSW1sdVpHVjRJaXdnSW5CaGJtUmhjMTkwZVhCbElqb2dJblZ1YVdOdlpHVWlMQ0FpYm5WdGNIbGZkSGx3WlNJNklDSnZZbXBsWTNRaUxDQWliV1YwWVdSaGRHRWlPaUJ1ZFd4c2ZTd2dleUp1WVcxbElqb2dJbXhsZG1Wc1h6QWlMQ0FpWm1sbGJHUmZibUZ0WlNJNklDSnNaWFpsYkY4d0lpd2dJbkJoYm1SaGMxOTBlWEJsSWpvZ0luVnVhV052WkdVaUxDQWliblZ0Y0hsZmRIbHdaU0k2SUNKdlltcGxZM1FpTENBaWJXVjBZV1JoZEdFaU9pQnVkV3hzZlN3Z2V5SnVZVzFsSWpvZ2JuVnNiQ3dnSW1acFpXeGtYMjVoYldVaU9pQWlYMTlwYm1SbGVGOXNaWFpsYkY4d1gxOGlMQ0FpY0dGdVpHRnpYM1I1Y0dVaU9pQWlkVzVwWTI5a1pTSXNJQ0p1ZFcxd2VWOTBlWEJsSWpvZ0ltOWlhbVZqZENJc0lDSnRaWFJoWkdGMFlTSTZJRzUxYkd4OVhTd2dJbU55WldGMGIzSWlPaUI3SW14cFluSmhjbmtpT2lBaWNIbGhjbkp2ZHlJc0lDSjJaWEp6YVc5dUlqb2dJakl4TGpBdU1DSjlMQ0FpY0dGdVpHRnpYM1psY25OcGIyNGlPaUFpTWk0eUxqTWlmUUFBQmdBQUFIQmhibVJoY3dBQUJBQUFBS1FBQUFCb0FBQUFQQUFBQUFRQUFBQjgvLy8vQUFBQkJSQUFBQUFrQUFBQUJBQUFBQUFBQUFBUkFBQUFYMTlwYm1SbGVGOXNaWFpsYkY4d1gxOEFBQUI4Ly8vL3NQLy8vd0FBQVFVUUFBQUFHQUFBQUFRQUFBQUFBQUFBQndBQUFHeGxkbVZzWHpBQXBQLy8vOWovLy84QUFBRUZFQUFBQUJnQUFBQUVBQUFBQUFBQUFBVUFBQUJwYm1SbGVBQUFBTXovLy84UUFCUUFDQUFHQUFjQURBQUFBQkFBRUFBQUFBQUFBUVVRQUFBQUdBQUFBQVFBQUFBQUFBQUFBUUFBQUdFQUFBQUVBQVFBQkFBQUFBQUFBQUE9ABggcGFycXVldC1jcHAtYXJyb3cgdmVyc2lvbiAyMS4wLjAZTBwAABwAABwAABwAAABLCgAAUEFSMQ=="}

0 commit comments

Comments
 (0)