Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/base/stest.ts
13389 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
import * as fs from 'fs';
6
import path from 'path';
7
import { Config, ExperimentBasedConfig, ExperimentBasedConfigType } from '../../src/platform/configuration/common/configurationService';
8
import { EmbeddingType } from '../../src/platform/embeddings/common/embeddingsComputer';
9
import { ILogTarget, LogLevel } from '../../src/platform/log/common/logService';
10
import { ISimulationTestContext } from '../../src/platform/simulationTestContext/common/simulationTestContext';
11
import { TestingServiceCollection } from '../../src/platform/test/node/services';
12
import { createServiceIdentifier } from '../../src/util/common/services';
13
import { grepStrToRegex } from '../simulation/shared/grepFilter';
14
import { EXPLICIT_LOG_TAG, IMPLICIT_LOG_TAG, ITestLocation, IWrittenFile, SIMULATION_EXPLICIT_LOG_FILENAME, SIMULATION_IMPLICIT_LOG_FILENAME, SimulationTestOutcome } from '../simulation/shared/sharedTypes';
15
import { computeSHA256 } from './hash';
16
import { SimulationOptions } from './simulationOptions';
17
export { REPO_ROOT } from '../util';
18
19
export interface SimulationTestFunction {
20
(testingServiceCollection: TestingServiceCollection): Promise<unknown> | unknown;
21
}
22
23
export interface ISimulationTestOptions {
24
optional?: boolean;
25
skip?: (opts: SimulationOptions) => boolean;
26
location?: ITestLocation;
27
conversationPath?: string;
28
scenarioFolderPath?: string;
29
stateFile?: string;
30
}
31
32
export class SimulationTestOptions {
33
public get optional(): boolean {
34
if (this._suiteOpts.optional) {
35
return true;
36
}
37
return this._opts.optional ?? false;
38
}
39
40
public skip(opts: SimulationOptions): boolean {
41
if (this._suiteOpts.skip(opts)) {
42
return true;
43
}
44
return this.mySkip(opts);
45
}
46
47
private _cachedMySkip: boolean | undefined = undefined;
48
private mySkip(opts: SimulationOptions): boolean {
49
if (this._cachedMySkip === undefined) {
50
this._cachedMySkip = this._opts.skip?.(opts) ?? false;
51
}
52
return this._cachedMySkip;
53
}
54
55
public get location(): ITestLocation | undefined {
56
return this._opts.location;
57
}
58
59
public get conversationPath(): string | undefined {
60
return this._opts.conversationPath;
61
}
62
63
public get scenarioFolderPath() {
64
return this._opts.scenarioFolderPath;
65
}
66
67
public get stateFile() {
68
return this._opts.stateFile;
69
}
70
71
constructor(
72
private readonly _opts: ISimulationTestOptions,
73
private readonly _suiteOpts: SimulationSuiteOptions
74
) { }
75
}
76
77
export interface ISimulationTestDescriptor {
78
79
/**
80
* This is used to capture the test scenario description itself.
81
*/
82
readonly description: string;
83
84
/**
85
* The programming language used for the test.
86
*
87
* If not set, may be inherited from the suite this test in if the suite descriptor specifies the language.
88
*/
89
readonly language?: string;
90
91
/**
92
* The model used for the test.
93
*/
94
readonly model?: string;
95
96
/**
97
* The embeddings model used for the test.
98
*/
99
readonly embeddingType?: EmbeddingType;
100
101
/**
102
* Setting configurations defined for the test
103
*/
104
readonly configurations?: Configuration<any>[];
105
106
/**
107
* Non-extension settings configurations defined for the test
108
*/
109
readonly nonExtensionConfigurations?: NonExtensionConfiguration[] | undefined;
110
111
/**
112
* Arbitrary attributes that will be serialised to the metadata.json file.
113
*/
114
readonly attributes?: Record<string, string | number>;
115
}
116
117
export type NonExtensionConfiguration = [string, any];
118
119
export type Configuration<T> = { key: ExperimentBasedConfig<ExperimentBasedConfigType> | Config<T>; value: T };
120
121
export class SimulationTest {
122
123
public readonly options: SimulationTestOptions;
124
public readonly description: string;
125
public readonly language: string | undefined;
126
public readonly model: string | undefined;
127
public readonly embeddingType: EmbeddingType | undefined;
128
public readonly configurations: Configuration<any>[] | undefined;
129
public readonly nonExtensionConfigurations: NonExtensionConfiguration[] | undefined;
130
public readonly attributes: Record<string, string | number> | undefined;
131
132
constructor(
133
descriptor: ISimulationTestDescriptor,
134
options: ISimulationTestOptions,
135
public readonly suite: SimulationSuite,
136
private readonly _runner: SimulationTestFunction,
137
) {
138
this.description = descriptor.description;
139
this.language = descriptor.language;
140
this.model = descriptor.model;
141
this.embeddingType = descriptor.embeddingType;
142
this.configurations = descriptor.configurations;
143
this.nonExtensionConfigurations = descriptor.nonExtensionConfigurations;
144
this.attributes = descriptor.attributes;
145
this.options = new SimulationTestOptions(options, suite.options);
146
}
147
148
public get fullName(): string {
149
return `${this.suite.fullName} ${this.language ? `[${this.language}] ` : ''}- ${this.description}${this.model ? ` - (${this.model})` : ''}${this.embeddingType ? ` - (${this.embeddingType})` : ''}`;
150
}
151
152
public get outcomeCategory(): string {
153
return this.suite.outcomeCategory;
154
}
155
156
public get outcomeFileName(): string {
157
return getOutcomeFileName(this.fullName);
158
}
159
160
public run(testingServiceCollection: TestingServiceCollection): Promise<unknown> {
161
return Promise.resolve(this._runner(testingServiceCollection));
162
}
163
164
toString(): string {
165
return `SimulationTest: ${this.fullName}`;
166
}
167
}
168
169
export function getOutcomeFileName(testName: string): string {
170
let suffix = '';
171
if (testName.endsWith(' - (gpt-4)')) {
172
testName = testName.substring(0, testName.length - 10);
173
suffix = '-gpt-4';
174
} else if (testName.endsWith(' - (gpt-3.5-turbo)')) {
175
testName = testName.substring(0, testName.length - 18);
176
suffix = '-gpt-3.5-turbo';
177
}
178
const result = toDirname(testName);
179
return `${result.substring(0, 60)}${suffix}.json`.replace(/-+/g, '-');
180
}
181
182
export interface ISimulationSuiteOptions {
183
optional?: boolean;
184
skip?: (opts: SimulationOptions) => boolean;
185
location?: ITestLocation;
186
}
187
188
export class SimulationSuiteOptions {
189
public get optional(): boolean {
190
return this._opts.optional ?? false;
191
}
192
193
private _cachedSkip: boolean | undefined = undefined;
194
public skip(opts: SimulationOptions): boolean {
195
if (this._cachedSkip === undefined) {
196
this._cachedSkip = this._opts.skip?.(opts) ?? false;
197
}
198
return this._cachedSkip;
199
}
200
201
public get location(): ITestLocation | undefined {
202
return this._opts.location;
203
}
204
205
constructor(
206
private readonly _opts: ISimulationSuiteOptions
207
) { }
208
}
209
210
export type ExtHostDescriptor = boolean; // todo: more things like extension config later
211
212
export interface ISimulationSuiteDescriptor {
213
214
/***
215
* This is used to group tests together.
216
* If using a slashCommand, use the command name else use "generic"
217
*/
218
readonly title: string;
219
220
221
/***
222
* This is used to capture the test scenario scope.
223
* Example: e2e, prompt, generate etc.
224
*/
225
readonly subtitle?: string;
226
readonly location: 'inline' | 'panel' | 'external' | 'context';
227
228
/**
229
* The programming language this suite tests.
230
*
231
* The test within the suite will also have this language if they do not specify a language in their descriptor {@link ISimulationTestDescriptor}.
232
*/
233
readonly language?: string;
234
235
/**
236
* Settings that override default settings in configuration service.
237
*
238
* These settings can further be overridden by the test itself.
239
*/
240
readonly configurations?: Configuration<any>[];
241
242
/**
243
* Non-extension settings configurations defined for the test
244
*/
245
readonly nonExtensionConfigurations?: NonExtensionConfiguration[] | undefined;
246
247
/**
248
* Set to true to run in a real VS Code extension host.
249
*/
250
readonly extHost?: ExtHostDescriptor;
251
}
252
253
export class SimulationSuite {
254
public readonly options: SimulationSuiteOptions;
255
256
public readonly language: string | undefined;
257
258
private readonly _title: string;
259
private readonly _subtitle: string | undefined;
260
private readonly _location: 'inline' | 'panel' | 'external' | 'context';
261
262
public readonly configurations: Configuration<any>[] | undefined;
263
public readonly nonExtensionConfigurations: NonExtensionConfiguration[] | undefined;
264
public readonly extHost: ExtHostDescriptor | undefined;
265
266
constructor(
267
descriptor: ISimulationSuiteDescriptor,
268
opts: ISimulationSuiteOptions = {},
269
public readonly tests: SimulationTest[] = [],
270
) {
271
this._title = descriptor.title;
272
this._subtitle = descriptor.subtitle;
273
this._location = descriptor.location;
274
this.language = descriptor.language;
275
this.configurations = descriptor.configurations;
276
this.nonExtensionConfigurations = descriptor.nonExtensionConfigurations;
277
this.options = new SimulationSuiteOptions(opts);
278
}
279
280
public get fullName(): string {
281
return `${this._title} ${this._subtitle ? `(${this._subtitle}) ` : ''}[${this._location}]`;
282
}
283
284
public get outcomeCategory(): string {
285
return `${this._title}${this._subtitle ? `-${this._subtitle}` : ''}-${this._location}`;
286
}
287
}
288
289
export type SimulationTestFilter = (test: SimulationTest) => boolean;
290
export function createSimulationTestFilter(grep?: string[] | string, omitGrep?: string): SimulationTestFilter {
291
const filters: ((test: SimulationTest) => boolean)[] = [];
292
if (grep) {
293
294
if (typeof grep === 'string') {
295
let trimmedGrep = grep.trim();
296
const isSuiteNameSearch = trimmedGrep.startsWith('!s:');
297
if (isSuiteNameSearch) {
298
trimmedGrep = trimmedGrep.replace(/^!s:/, '');
299
}
300
const grepRegex = grepStrToRegex(trimmedGrep);
301
filters.push((test) => isSuiteNameSearch ? grepRegex.test(test.suite.fullName) : grepRegex.test(test.fullName));
302
} else {
303
const grepArr = Array.isArray(grep) ? grep : [grep];
304
for (const grep of grepArr) {
305
const grepLowerCase = String(grep).toLowerCase();
306
const grepFilter = (str: string) => str.toLowerCase().indexOf(grepLowerCase) >= 0;
307
filters.push((test) => grepFilter(test.fullName));
308
}
309
}
310
}
311
312
if (omitGrep) {
313
const omitGrepRegex = grepStrToRegex(omitGrep);
314
filters.push((test) => !omitGrepRegex.test(test.fullName));
315
}
316
return (test: SimulationTest) => filters.every(shouldRunTest => shouldRunTest(test));
317
}
318
319
class SimulationTestsRegistryClass {
320
private readonly defaultSuite: SimulationSuite = new SimulationSuite({ title: 'generic', location: 'inline' });
321
private suites: SimulationSuite[] = [this.defaultSuite];
322
private currentSuite: SimulationSuite = this.defaultSuite;
323
private readonly testNames = new Set<string>();
324
325
private _inputPath: string | undefined;
326
public setInputPath(inputPath: string) {
327
this._inputPath = inputPath;
328
}
329
330
private _testPath: string | undefined;
331
private _filter: (test: SimulationTest) => boolean = () => true;
332
public setFilters(testPath?: string, grep?: string[] | string, omitGrep?: string) {
333
this._testPath = testPath;
334
this._filter = createSimulationTestFilter(grep, omitGrep);
335
}
336
337
public getAllSuites(): readonly SimulationSuite[] {
338
return this.suites;
339
}
340
341
public getAllTests(): readonly SimulationTest[] {
342
const allTests = this.suites.reduce((prev, curr) => prev.concat(curr.tests), [] as SimulationTest[]);
343
const testsToRun = allTests.filter(this._filter).sort((t0, t1) => t0.fullName.localeCompare(t1.fullName));
344
return testsToRun;
345
}
346
347
private _allowTestReregistration = false;
348
349
public allowTestReregistration() {
350
this._allowTestReregistration = true;
351
}
352
353
public registerTest(testDescriptor: ISimulationTestDescriptor, options: ISimulationTestOptions, runner: SimulationTestFunction): void {
354
if (testDescriptor.language === undefined && this.currentSuite.language) {
355
testDescriptor = { ...testDescriptor, language: this.currentSuite.language };
356
}
357
358
// inherit configurations from suite
359
if (this.currentSuite.configurations !== undefined) {
360
const updatedConfigurations =
361
testDescriptor.configurations === undefined
362
? this.currentSuite.configurations
363
: [...this.currentSuite.configurations, ...testDescriptor.configurations];
364
testDescriptor = { ...testDescriptor, configurations: updatedConfigurations };
365
}
366
367
if (this.currentSuite.nonExtensionConfigurations !== undefined) {
368
const updatedNonExtConfig: NonExtensionConfiguration[] = this.currentSuite.nonExtensionConfigurations.slice(0);
369
updatedNonExtConfig.push(...testDescriptor.nonExtensionConfigurations ?? []);
370
testDescriptor = { ...testDescriptor, nonExtensionConfigurations: updatedNonExtConfig };
371
}
372
373
// remove newlines, carriage returns, bad whitespace, etc
374
testDescriptor = { ...testDescriptor, description: testDescriptor.description.replace(/\s+/g, ' ') };
375
376
// force a length of 100 chars for a stest name
377
if (testDescriptor.description.length > 100) {
378
testDescriptor = { ...testDescriptor, description: testDescriptor.description.substring(0, 100) + '…' };
379
}
380
381
const test = new SimulationTest(testDescriptor, options, this.currentSuite, runner);
382
// change this validation up
383
if (this.testNames.has(test.fullName) && !this._allowTestReregistration) {
384
throw new Error(`Cannot have two tests with the same name: ${test.fullName}`);
385
}
386
this.testNames.add(test.fullName);
387
388
this.currentSuite.tests.push(test);
389
}
390
391
public registerSuite(descriptor: ISimulationSuiteDescriptor, options: ISimulationSuiteOptions, factory: (inputPath?: string) => void) {
392
if (this._testPath && options.location !== undefined) {
393
394
const testBasename = path.basename(options.location.path);
395
const testBasenameWithoutExtension = testBasename.replace(/\.[^/.]+$/, '');
396
397
if (this._testPath !== testBasename && this._testPath !== testBasenameWithoutExtension) {
398
return;
399
}
400
}
401
402
const suite = new SimulationSuite(descriptor, options);
403
404
function suiteId(s: SimulationSuite): string {
405
return s.options.location?.path + '###' + s.fullName;
406
}
407
this.suites = this.suites.filter(s => suiteId(s) !== suiteId(suite)); // When re-registering a suite, delete the old one
408
this.suites.push(suite);
409
this.invokeSuiteFactory(suite, factory);
410
}
411
412
private invokeSuiteFactory(suite: SimulationSuite, factory: (inputPath?: string) => void) {
413
try {
414
this.currentSuite = suite;
415
factory(this._inputPath);
416
} finally {
417
this.currentSuite = this.defaultSuite;
418
}
419
}
420
}
421
422
export const SimulationTestsRegistry = new SimulationTestsRegistryClass();
423
424
function captureLocation(fn: Function): ITestLocation | undefined {
425
try {
426
const err = new Error();
427
Error.captureStackTrace(err, fn);
428
throw err;
429
} catch (e) {
430
431
const stack = (<string[]>e.stack.split('\n')).at(1);
432
if (!stack) {
433
// It looks like sometimes the stack is empty,
434
// so let's add a fallback case
435
return captureLocationUsingClassicalWay();
436
}
437
return extractPositionFromStackTraceLine(stack);
438
}
439
440
function captureLocationUsingClassicalWay(): ITestLocation | undefined {
441
try {
442
throw new Error();
443
} catch (e) {
444
// Error:
445
// at captureLocationUsingClassicalWay (/Users/alex/src/vscode-copilot/test/base/stest.ts:398:10)
446
// at captureLocation (/Users/alex/src/vscode-copilot/test/base/stest.ts:374:11)
447
// at stest (/Users/alex/src/vscode-copilot/test/base/stest.ts:467:84)
448
// at /Users/alex/src/vscode-copilot/test/codeMapper/codeMapper.stest.ts:22:2
449
const stack = (<string[]>e.stack.split('\n')).at(4);
450
if (!stack) {
451
console.log(`No stack in captureLocation`);
452
console.log(e.stack);
453
return undefined;
454
}
455
return extractPositionFromStackTraceLine(stack);
456
}
457
}
458
459
function extractPositionFromStackTraceLine(stack: string): ITestLocation | undefined {
460
const r1 = /\((.+):(\d+):(\d+)\)/;
461
const r2 = /at (.+):(\d+):(\d+)/;
462
const match = stack.match(r1) ?? stack.match(r2);
463
if (!match) {
464
console.log(`No matches in stack for captureLocation`);
465
console.log(stack);
466
return undefined;
467
}
468
469
return {
470
path: match[1],
471
position: {
472
line: Number(match[2]) - 1,
473
character: Number(match[3]) - 1,
474
}
475
};
476
}
477
}
478
479
/**
480
* @remarks DO NOT FORGET to register the test file in `simulationTests.ts` for local test files
481
*/
482
export function ssuite(descriptor: ISimulationSuiteDescriptor, factory: (inputPath?: string) => void) {
483
SimulationTestsRegistry.registerSuite(descriptor, { optional: false, location: captureLocation(ssuite) }, factory);
484
}
485
ssuite.optional = function (skip: (opts: SimulationOptions) => boolean, descriptor: ISimulationSuiteDescriptor, factory: (inputPath?: string) => void) {
486
SimulationTestsRegistry.registerSuite(descriptor, { optional: true, skip, location: captureLocation(ssuite.optional) }, factory);
487
};
488
ssuite.skip = function (descriptor: ISimulationSuiteDescriptor, factory: (inputPath?: string) => void) {
489
SimulationTestsRegistry.registerSuite(descriptor, { optional: true, skip: (_: SimulationOptions) => true, location: captureLocation(ssuite.skip) }, factory);
490
};
491
492
/**
493
* The test function will receive as first argument a context.
494
*
495
* On the context, you will find a good working ChatMLFetcher which uses caching
496
* and a caching slot which matches the run number.
497
*
498
* You will also find `SimulationTestRuntime` on the context, which allows you
499
* to use logging in your test or write files to the test outcome directory.
500
*/
501
export function stest(testDescriptor: string | ISimulationTestDescriptor, runner: SimulationTestFunction, opts?: ISimulationTestOptions) {
502
testDescriptor = typeof testDescriptor === 'string' ? { description: testDescriptor } : testDescriptor;
503
SimulationTestsRegistry.registerTest(testDescriptor, { optional: false, location: captureLocation(stest), ...opts }, runner);
504
}
505
stest.optional = function (skip: () => boolean, testDescriptor: ISimulationTestDescriptor, runner: SimulationTestFunction, opts?: ISimulationTestOptions) {
506
SimulationTestsRegistry.registerTest(testDescriptor, { optional: true, skip, location: captureLocation(stest.optional), ...opts }, runner);
507
};
508
stest.skip = function (testDescriptor: ISimulationTestDescriptor, runner: SimulationTestFunction, opts?: ISimulationTestOptions) {
509
SimulationTestsRegistry.registerTest(testDescriptor, { optional: true, skip: () => true, location: captureLocation(stest.skip), ...opts }, runner);
510
};
511
512
export const ISimulationTestRuntime = createServiceIdentifier<ISimulationTestRuntime>('ISimulationTestRuntime');
513
514
export interface ISimulationTestRuntime extends ILogTarget, ISimulationTestContext {
515
516
logIt(level: LogLevel, metadataStr: string, ...extra: any[]): void;
517
shouldLog(level: LogLevel): boolean | undefined;
518
log(message: string, err?: any): void;
519
flushLogs(): Promise<void>;
520
writeFile(filename: string, contents: Uint8Array | string, tag: string): Promise<string>;
521
getWrittenFiles(): IWrittenFile[];
522
getOutcome(): SimulationTestOutcome | undefined;
523
setOutcome(outcome: SimulationTestOutcome): void;
524
getExplicitScore(): number | undefined;
525
setExplicitScore(score: number): void;
526
}
527
528
export class SimulationTestRuntime implements ISimulationTestRuntime {
529
530
declare readonly _serviceBrand: undefined;
531
532
private readonly explicitLogMessages: string[] = [];
533
private readonly implicitLogMessages: string[] = [];
534
private readonly writtenFiles: IWrittenFile[] = [];
535
private score?: number;
536
private outcome: SimulationTestOutcome | undefined = undefined;
537
538
constructor(
539
private readonly baseDir: string,
540
private readonly testOutcomeDir: string,
541
protected readonly runNumber: number
542
) { }
543
544
public readonly isInSimulationTests = true;
545
546
public logIt(level: LogLevel, metadataStr: string, ...extra: any[]): void {
547
const timestamp = new Date().toISOString();
548
this.implicitLogMessages.push(`[${timestamp}] ${metadataStr} ${extra.join(' ')}`);
549
}
550
551
public shouldLog(level: LogLevel): boolean | undefined {
552
return undefined;
553
}
554
555
public log(message: string, err?: any): void {
556
if (err) {
557
message += ' ' + (err.stack ? String(err.stack) : String(err));
558
}
559
this.explicitLogMessages.push(message);
560
}
561
562
public async flushLogs(): Promise<void> {
563
if (this.explicitLogMessages.length > 0) {
564
await this.writeFile(SIMULATION_EXPLICIT_LOG_FILENAME, this.explicitLogMessages.join('\n'), EXPLICIT_LOG_TAG);
565
}
566
if (this.implicitLogMessages.length > 0) {
567
await this.writeFile(SIMULATION_IMPLICIT_LOG_FILENAME, this.implicitLogMessages.join('\n'), IMPLICIT_LOG_TAG);
568
}
569
}
570
571
public async writeFile(filename: string, contents: Uint8Array | string, tag: string): Promise<string> {
572
const dest = this._findUniqueFilename(
573
path.join(this.testOutcomeDir, this.massageFilename(filename))
574
);
575
576
const relativePath = path.relative(this.baseDir, dest);
577
this.writtenFiles.push({
578
relativePath,
579
tag
580
});
581
582
await fs.promises.mkdir(path.dirname(dest), { recursive: true });
583
await fs.promises.writeFile(dest, contents);
584
return relativePath;
585
}
586
587
protected massageFilename(filename: string): string {
588
return `${(this.runNumber).toString().padStart(2, '0')}-${filename}`;
589
}
590
591
/**
592
* Generate a new filePath in case this filePath already exists.
593
*/
594
private _findUniqueFilename(initialFilePath: string): string {
595
for (let i = 0; i < 1000; i++) {
596
let filePath = initialFilePath;
597
if (i > 0) {
598
// This file was already written, we'll rename it to <basename>.X.<ext>
599
const ext = path.extname(initialFilePath);
600
const basename = initialFilePath.substring(0, initialFilePath.length - ext.length);
601
filePath = `${basename}.${i}${ext}`;
602
}
603
const relativePath = path.relative(this.baseDir, filePath);
604
const exists = this.writtenFiles.find(x => x.relativePath === relativePath);
605
if (!exists) {
606
return filePath;
607
}
608
}
609
return initialFilePath;
610
}
611
612
public getWrittenFiles(): IWrittenFile[] {
613
return this.writtenFiles.slice(0);
614
}
615
616
public getOutcome(): SimulationTestOutcome | undefined {
617
return this.outcome;
618
}
619
620
public setOutcome(outcome: SimulationTestOutcome) {
621
this.outcome = outcome;
622
}
623
624
public getExplicitScore(): number | undefined {
625
return this.score;
626
}
627
628
public setExplicitScore(score: number) {
629
this.score = score;
630
}
631
}
632
633
const FILENAME_LIMIT = 125;
634
635
export function toDirname(testName: string): string {
636
const filename = testName.replace(/[^a-zA-Z0-9]/g, '-').replace(/-+/g, '-').toLowerCase();
637
if (filename.length > FILENAME_LIMIT) { // windows file names can not exceed 255 chars and path length limits, so keep it short
638
return `${filename.substring(0, FILENAME_LIMIT)}-${computeSHA256(filename).substring(0, 8)}`;
639
}
640
return filename;
641
}
642