ccqa 1.58.2 → 1.59.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/bin/ccqa.mjs +1506 -1120
- package/dist/hub-client/index.d.cts +2 -2
- package/dist/hub-client/index.d.mts +2 -2
- package/dist/package.json +1 -1
- package/dist/runtime/judge.cjs +53 -35
- package/dist/runtime/judge.mjs +53 -35
- package/package.json +1 -1
|
@@ -35,9 +35,9 @@ declare const RunSchema: z.ZodObject<{
|
|
|
35
35
|
running: "running";
|
|
36
36
|
}>;
|
|
37
37
|
kind: z.ZodDefault<z.ZodEnum<{
|
|
38
|
-
record: "record";
|
|
39
38
|
run: "run";
|
|
40
39
|
drift: "drift";
|
|
40
|
+
record: "record";
|
|
41
41
|
}>>;
|
|
42
42
|
drift: z.ZodDefault<z.ZodNullable<z.ZodObject<{
|
|
43
43
|
specs: z.ZodNumber;
|
|
@@ -831,9 +831,9 @@ type ReportSpecResult = z.infer<typeof ReportSpecResultSchema>;
|
|
|
831
831
|
declare const RunReportDataSchema: z.ZodObject<{
|
|
832
832
|
schemaVersion: z.ZodLiteral<1>;
|
|
833
833
|
kind: z.ZodDefault<z.ZodEnum<{
|
|
834
|
-
record: "record";
|
|
835
834
|
run: "run";
|
|
836
835
|
drift: "drift";
|
|
836
|
+
record: "record";
|
|
837
837
|
}>>;
|
|
838
838
|
createdAt: z.ZodString;
|
|
839
839
|
runId: z.ZodNullable<z.ZodString>;
|
|
@@ -35,9 +35,9 @@ declare const RunSchema: z.ZodObject<{
|
|
|
35
35
|
running: "running";
|
|
36
36
|
}>;
|
|
37
37
|
kind: z.ZodDefault<z.ZodEnum<{
|
|
38
|
-
record: "record";
|
|
39
38
|
run: "run";
|
|
40
39
|
drift: "drift";
|
|
40
|
+
record: "record";
|
|
41
41
|
}>>;
|
|
42
42
|
drift: z.ZodDefault<z.ZodNullable<z.ZodObject<{
|
|
43
43
|
specs: z.ZodNumber;
|
|
@@ -831,9 +831,9 @@ type ReportSpecResult = z.infer<typeof ReportSpecResultSchema>;
|
|
|
831
831
|
declare const RunReportDataSchema: z.ZodObject<{
|
|
832
832
|
schemaVersion: z.ZodLiteral<1>;
|
|
833
833
|
kind: z.ZodDefault<z.ZodEnum<{
|
|
834
|
-
record: "record";
|
|
835
834
|
run: "run";
|
|
836
835
|
drift: "drift";
|
|
836
|
+
record: "record";
|
|
837
837
|
}>>;
|
|
838
838
|
createdAt: z.ZodString;
|
|
839
839
|
runId: z.ZodNullable<z.ZodString>;
|
package/dist/package.json
CHANGED
package/dist/runtime/judge.cjs
CHANGED
|
@@ -415,7 +415,7 @@ function formatDuration(ms) {
|
|
|
415
415
|
return `${minutes} minute${minutes === 1 ? "" : "s"}`;
|
|
416
416
|
}
|
|
417
417
|
async function invokeClaudeStreaming(options, onEvent) {
|
|
418
|
-
const { prompt, systemPrompt, allowedTools, disableThinking = false, mcpServers, maxTurns, timeoutMs, env, model, cwd, onAbAction, onAbActionFailed, silenceBashLog = false, envScrubMap = [], relaxAbConstraints = false, additionalDirectories } = options;
|
|
418
|
+
const { prompt, systemPrompt, allowedTools, disableThinking = false, effort, bashGuard, mcpServers, maxTurns, timeoutMs, env, model, cwd, onAbAction, onAbActionFailed, beforeAbCommand, silenceBashLog = false, envScrubMap = [], relaxAbConstraints = false, additionalDirectories } = options;
|
|
419
419
|
const resolvedModel = resolveModel(model);
|
|
420
420
|
const mergedEnv = buildInvocationEnv(env);
|
|
421
421
|
const abortController = new AbortController();
|
|
@@ -443,52 +443,35 @@ async function invokeClaudeStreaming(options, onEvent) {
|
|
|
443
443
|
env: mergedEnv,
|
|
444
444
|
...mcpServers ? { mcpServers } : {},
|
|
445
445
|
...disableThinking ? { thinking: { type: "disabled" } } : {},
|
|
446
|
-
|
|
446
|
+
...effort ? { effort } : {},
|
|
447
|
+
hooks: onAbAction || onAbActionFailed || bashGuard || beforeAbCommand ? {
|
|
447
448
|
PreToolUse: [{ hooks: [async (input) => {
|
|
448
449
|
if (input.hook_event_name !== "PreToolUse") return {};
|
|
449
450
|
if (input.tool_name !== "Bash") return {};
|
|
450
451
|
const cmd = input.tool_input?.["command"];
|
|
451
452
|
if (typeof cmd !== "string") return {};
|
|
453
|
+
const guarded = bashGuard?.(cmd);
|
|
454
|
+
if (guarded) return {
|
|
455
|
+
decision: "block",
|
|
456
|
+
reason: guarded
|
|
457
|
+
};
|
|
452
458
|
if (!relaxAbConstraints) {
|
|
453
|
-
|
|
459
|
+
const reason = abCommandBlockReason(cmd, envScrubMap);
|
|
460
|
+
if (reason !== null) return {
|
|
454
461
|
decision: "block",
|
|
455
|
-
reason
|
|
462
|
+
reason
|
|
456
463
|
};
|
|
457
|
-
|
|
464
|
+
const checkpoint = beforeAbCommand?.(extractCcqaStepFromBashCommand(cmd) ?? void 0);
|
|
465
|
+
if (checkpoint) return {
|
|
458
466
|
decision: "block",
|
|
459
|
-
reason:
|
|
460
|
-
};
|
|
461
|
-
const bareTag = findPositionalBareTag(cmd);
|
|
462
|
-
if (bareTag !== null) return {
|
|
463
|
-
decision: "block",
|
|
464
|
-
reason: `\`find ${bareTag.locator}\` with a bare tag selector (\`${bareTag.selector}\`) is rejected: it matches every <${bareTag.selector}> on the page and is non-deterministic on replay. Pass a specific attribute selector instead, e.g. \`find ${bareTag.locator} "[aria-label='...']" ${bareTag.action}\` or \`find ${bareTag.locator} "[data-qa='...']" ${bareTag.action}\`. Take a fresh snapshot to find the right attribute.`
|
|
465
|
-
};
|
|
466
|
-
if (hasMultipleAbInvocations(cmd)) return {
|
|
467
|
-
decision: "block",
|
|
468
|
-
reason: "Run each `agent-browser` call as its own Bash command. Chaining multiple invocations with &&, ;, |, or || prevents ccqa from recording them as discrete steps and lets failed attempts leak into the trace. Issue one Bash tool call per agent-browser command."
|
|
469
|
-
};
|
|
470
|
-
if (hasErrorSuppression(cmd)) return {
|
|
471
|
-
decision: "block",
|
|
472
|
-
reason: "Do not suppress errors on `agent-browser` commands. Remove `|| true`, `|| :`, `2>/dev/null`, `; true`, and similar redirects so ccqa can detect failures and roll back unsuccessful attempts. Run the command standalone and let it surface its exit code."
|
|
473
|
-
};
|
|
474
|
-
const runProducedUrl = findRunProducedOpenUrl(cmd, envScrubMap);
|
|
475
|
-
if (runProducedUrl !== null) return {
|
|
476
|
-
decision: "block",
|
|
477
|
-
reason: `Do not open ${runProducedUrl} — that address was produced by this run (the id in it belongs to a record this run created), so the generated test would open a record later runs do not have. Reach the page the way a person does: click through from where the run already is. If the spec's instruction really names this exact address, put the id in a profile variable so it survives replay.`
|
|
467
|
+
reason: checkpoint
|
|
478
468
|
};
|
|
479
469
|
}
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
if ((ab !== null || assertMarker !== null) && onAbAction) {
|
|
470
|
+
const recorded = relaxAbConstraints ? null : abActionEventFromCommand(cmd);
|
|
471
|
+
if (recorded !== null && onAbAction) {
|
|
483
472
|
lastAbToolUseId = input.tool_use_id;
|
|
484
|
-
lastAbAnswerHolds =
|
|
485
|
-
|
|
486
|
-
onAbAction({
|
|
487
|
-
...ab !== null ? { abAction: ab } : {},
|
|
488
|
-
...stepId ? { stepId } : {},
|
|
489
|
-
...assertMarker !== null ? { assertMarker } : {},
|
|
490
|
-
...hasCcqaSecretPrefix(cmd) ? { secret: true } : {}
|
|
491
|
-
});
|
|
473
|
+
lastAbAnswerHolds = recorded.holds;
|
|
474
|
+
onAbAction(recorded.event);
|
|
492
475
|
} else lastAbToolUseId = null;
|
|
493
476
|
return {};
|
|
494
477
|
}] }],
|
|
@@ -1076,6 +1059,41 @@ async function fireMockPreToolUseHooks(msg, options) {
|
|
|
1076
1059
|
for (const matcher of matchers) for (const hook of matcher.hooks) await hook(input, block.id, { signal: new AbortController().signal });
|
|
1077
1060
|
}
|
|
1078
1061
|
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Why the trace refuses this agent-browser command, or null when it may run.
|
|
1064
|
+
* Shared by the Bash hook and the trace's own batch tool, so both enforce the
|
|
1065
|
+
* same replayability contract.
|
|
1066
|
+
*/
|
|
1067
|
+
function abCommandBlockReason(cmd, envScrubMap = []) {
|
|
1068
|
+
if (isBlockedAbSubcommand(cmd)) return "This agent-browser subcommand is not allowed because it cannot be recorded as a structured test action. Use only the standard commands: click, check, fill, select, hover, press, wait, find (with role/text/label/placeholder/alt/title/testid/first/last/nth). Take a fresh snapshot to find the correct selector.";
|
|
1069
|
+
if (hasRefSelector(cmd)) return "@ref selectors (like @e14) are session-specific and change every run. They cannot be used in generated tests. Use one of the allowed selector formats instead: [aria-label='...'], text=..., [placeholder='...'], or [type='password']. Take a fresh snapshot and find the element's aria-label or visible text. If an allowed selector already clicks the element but nothing happens, the element is clipped by an inner scroll container: `scrollintoview` it (addressed by a CSS selector, not `text=`) and click again — a @ref would not have fixed that either.";
|
|
1070
|
+
const bareTag = findPositionalBareTag(cmd);
|
|
1071
|
+
if (bareTag !== null) return `\`find ${bareTag.locator}\` with a bare tag selector (\`${bareTag.selector}\`) is rejected: it matches every <${bareTag.selector}> on the page and is non-deterministic on replay. Pass a specific attribute selector instead, e.g. \`find ${bareTag.locator} "[aria-label='...']" ${bareTag.action}\` or \`find ${bareTag.locator} "[data-qa='...']" ${bareTag.action}\`. Take a fresh snapshot to find the right attribute.`;
|
|
1072
|
+
if (hasMultipleAbInvocations(cmd)) return "Run each `agent-browser` call as its own Bash command. Chaining multiple invocations with &&, ;, |, or || prevents ccqa from recording them as discrete steps and lets failed attempts leak into the trace. Issue one Bash tool call per agent-browser command.";
|
|
1073
|
+
if (hasErrorSuppression(cmd)) return "Do not suppress errors on `agent-browser` commands. Remove `|| true`, `|| :`, `2>/dev/null`, `; true`, and similar redirects so ccqa can detect failures and roll back unsuccessful attempts. Run the command standalone and let it surface its exit code.";
|
|
1074
|
+
const runProducedUrl = findRunProducedOpenUrl(cmd, envScrubMap);
|
|
1075
|
+
if (runProducedUrl !== null) return `Do not open ${runProducedUrl} — that address was produced by this run (the id in it belongs to a record this run created), so the generated test would open a record later runs do not have. Reach the page the way a person does: click through from where the run already is. If the spec's instruction really names this exact address, put the id in a profile variable so it survives replay.`;
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* What recording this command produces, and — for a marked probe — what its
|
|
1080
|
+
* output must say for the check to hold. Null when it records nothing.
|
|
1081
|
+
*/
|
|
1082
|
+
function abActionEventFromCommand(cmd) {
|
|
1083
|
+
const assertMarker = extractCcqaAssertFromBashCommand(cmd);
|
|
1084
|
+
const ab = extractAbActionFromBashCommand(cmd) ?? (assertMarker !== null ? extractObservationAbAction(cmd) : null);
|
|
1085
|
+
if (ab === null && assertMarker === null) return null;
|
|
1086
|
+
const stepId = extractCcqaStepFromBashCommand(cmd);
|
|
1087
|
+
return {
|
|
1088
|
+
event: {
|
|
1089
|
+
...ab !== null ? { abAction: ab } : {},
|
|
1090
|
+
...stepId ? { stepId } : {},
|
|
1091
|
+
...assertMarker !== null ? { assertMarker } : {},
|
|
1092
|
+
...hasCcqaSecretPrefix(cmd) ? { secret: true } : {}
|
|
1093
|
+
},
|
|
1094
|
+
holds: assertMarker === null ? null : markerHolds(assertMarker, ab)
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1079
1097
|
//#endregion
|
|
1080
1098
|
//#region src/drift/auth.ts
|
|
1081
1099
|
/**
|
package/dist/runtime/judge.mjs
CHANGED
|
@@ -414,7 +414,7 @@ function formatDuration(ms) {
|
|
|
414
414
|
return `${minutes} minute${minutes === 1 ? "" : "s"}`;
|
|
415
415
|
}
|
|
416
416
|
async function invokeClaudeStreaming(options, onEvent) {
|
|
417
|
-
const { prompt, systemPrompt, allowedTools, disableThinking = false, mcpServers, maxTurns, timeoutMs, env, model, cwd, onAbAction, onAbActionFailed, silenceBashLog = false, envScrubMap = [], relaxAbConstraints = false, additionalDirectories } = options;
|
|
417
|
+
const { prompt, systemPrompt, allowedTools, disableThinking = false, effort, bashGuard, mcpServers, maxTurns, timeoutMs, env, model, cwd, onAbAction, onAbActionFailed, beforeAbCommand, silenceBashLog = false, envScrubMap = [], relaxAbConstraints = false, additionalDirectories } = options;
|
|
418
418
|
const resolvedModel = resolveModel(model);
|
|
419
419
|
const mergedEnv = buildInvocationEnv(env);
|
|
420
420
|
const abortController = new AbortController();
|
|
@@ -442,52 +442,35 @@ async function invokeClaudeStreaming(options, onEvent) {
|
|
|
442
442
|
env: mergedEnv,
|
|
443
443
|
...mcpServers ? { mcpServers } : {},
|
|
444
444
|
...disableThinking ? { thinking: { type: "disabled" } } : {},
|
|
445
|
-
|
|
445
|
+
...effort ? { effort } : {},
|
|
446
|
+
hooks: onAbAction || onAbActionFailed || bashGuard || beforeAbCommand ? {
|
|
446
447
|
PreToolUse: [{ hooks: [async (input) => {
|
|
447
448
|
if (input.hook_event_name !== "PreToolUse") return {};
|
|
448
449
|
if (input.tool_name !== "Bash") return {};
|
|
449
450
|
const cmd = input.tool_input?.["command"];
|
|
450
451
|
if (typeof cmd !== "string") return {};
|
|
452
|
+
const guarded = bashGuard?.(cmd);
|
|
453
|
+
if (guarded) return {
|
|
454
|
+
decision: "block",
|
|
455
|
+
reason: guarded
|
|
456
|
+
};
|
|
451
457
|
if (!relaxAbConstraints) {
|
|
452
|
-
|
|
458
|
+
const reason = abCommandBlockReason(cmd, envScrubMap);
|
|
459
|
+
if (reason !== null) return {
|
|
453
460
|
decision: "block",
|
|
454
|
-
reason
|
|
461
|
+
reason
|
|
455
462
|
};
|
|
456
|
-
|
|
463
|
+
const checkpoint = beforeAbCommand?.(extractCcqaStepFromBashCommand(cmd) ?? void 0);
|
|
464
|
+
if (checkpoint) return {
|
|
457
465
|
decision: "block",
|
|
458
|
-
reason:
|
|
459
|
-
};
|
|
460
|
-
const bareTag = findPositionalBareTag(cmd);
|
|
461
|
-
if (bareTag !== null) return {
|
|
462
|
-
decision: "block",
|
|
463
|
-
reason: `\`find ${bareTag.locator}\` with a bare tag selector (\`${bareTag.selector}\`) is rejected: it matches every <${bareTag.selector}> on the page and is non-deterministic on replay. Pass a specific attribute selector instead, e.g. \`find ${bareTag.locator} "[aria-label='...']" ${bareTag.action}\` or \`find ${bareTag.locator} "[data-qa='...']" ${bareTag.action}\`. Take a fresh snapshot to find the right attribute.`
|
|
464
|
-
};
|
|
465
|
-
if (hasMultipleAbInvocations(cmd)) return {
|
|
466
|
-
decision: "block",
|
|
467
|
-
reason: "Run each `agent-browser` call as its own Bash command. Chaining multiple invocations with &&, ;, |, or || prevents ccqa from recording them as discrete steps and lets failed attempts leak into the trace. Issue one Bash tool call per agent-browser command."
|
|
468
|
-
};
|
|
469
|
-
if (hasErrorSuppression(cmd)) return {
|
|
470
|
-
decision: "block",
|
|
471
|
-
reason: "Do not suppress errors on `agent-browser` commands. Remove `|| true`, `|| :`, `2>/dev/null`, `; true`, and similar redirects so ccqa can detect failures and roll back unsuccessful attempts. Run the command standalone and let it surface its exit code."
|
|
472
|
-
};
|
|
473
|
-
const runProducedUrl = findRunProducedOpenUrl(cmd, envScrubMap);
|
|
474
|
-
if (runProducedUrl !== null) return {
|
|
475
|
-
decision: "block",
|
|
476
|
-
reason: `Do not open ${runProducedUrl} — that address was produced by this run (the id in it belongs to a record this run created), so the generated test would open a record later runs do not have. Reach the page the way a person does: click through from where the run already is. If the spec's instruction really names this exact address, put the id in a profile variable so it survives replay.`
|
|
466
|
+
reason: checkpoint
|
|
477
467
|
};
|
|
478
468
|
}
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
if ((ab !== null || assertMarker !== null) && onAbAction) {
|
|
469
|
+
const recorded = relaxAbConstraints ? null : abActionEventFromCommand(cmd);
|
|
470
|
+
if (recorded !== null && onAbAction) {
|
|
482
471
|
lastAbToolUseId = input.tool_use_id;
|
|
483
|
-
lastAbAnswerHolds =
|
|
484
|
-
|
|
485
|
-
onAbAction({
|
|
486
|
-
...ab !== null ? { abAction: ab } : {},
|
|
487
|
-
...stepId ? { stepId } : {},
|
|
488
|
-
...assertMarker !== null ? { assertMarker } : {},
|
|
489
|
-
...hasCcqaSecretPrefix(cmd) ? { secret: true } : {}
|
|
490
|
-
});
|
|
472
|
+
lastAbAnswerHolds = recorded.holds;
|
|
473
|
+
onAbAction(recorded.event);
|
|
491
474
|
} else lastAbToolUseId = null;
|
|
492
475
|
return {};
|
|
493
476
|
}] }],
|
|
@@ -1075,6 +1058,41 @@ async function fireMockPreToolUseHooks(msg, options) {
|
|
|
1075
1058
|
for (const matcher of matchers) for (const hook of matcher.hooks) await hook(input, block.id, { signal: new AbortController().signal });
|
|
1076
1059
|
}
|
|
1077
1060
|
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Why the trace refuses this agent-browser command, or null when it may run.
|
|
1063
|
+
* Shared by the Bash hook and the trace's own batch tool, so both enforce the
|
|
1064
|
+
* same replayability contract.
|
|
1065
|
+
*/
|
|
1066
|
+
function abCommandBlockReason(cmd, envScrubMap = []) {
|
|
1067
|
+
if (isBlockedAbSubcommand(cmd)) return "This agent-browser subcommand is not allowed because it cannot be recorded as a structured test action. Use only the standard commands: click, check, fill, select, hover, press, wait, find (with role/text/label/placeholder/alt/title/testid/first/last/nth). Take a fresh snapshot to find the correct selector.";
|
|
1068
|
+
if (hasRefSelector(cmd)) return "@ref selectors (like @e14) are session-specific and change every run. They cannot be used in generated tests. Use one of the allowed selector formats instead: [aria-label='...'], text=..., [placeholder='...'], or [type='password']. Take a fresh snapshot and find the element's aria-label or visible text. If an allowed selector already clicks the element but nothing happens, the element is clipped by an inner scroll container: `scrollintoview` it (addressed by a CSS selector, not `text=`) and click again — a @ref would not have fixed that either.";
|
|
1069
|
+
const bareTag = findPositionalBareTag(cmd);
|
|
1070
|
+
if (bareTag !== null) return `\`find ${bareTag.locator}\` with a bare tag selector (\`${bareTag.selector}\`) is rejected: it matches every <${bareTag.selector}> on the page and is non-deterministic on replay. Pass a specific attribute selector instead, e.g. \`find ${bareTag.locator} "[aria-label='...']" ${bareTag.action}\` or \`find ${bareTag.locator} "[data-qa='...']" ${bareTag.action}\`. Take a fresh snapshot to find the right attribute.`;
|
|
1071
|
+
if (hasMultipleAbInvocations(cmd)) return "Run each `agent-browser` call as its own Bash command. Chaining multiple invocations with &&, ;, |, or || prevents ccqa from recording them as discrete steps and lets failed attempts leak into the trace. Issue one Bash tool call per agent-browser command.";
|
|
1072
|
+
if (hasErrorSuppression(cmd)) return "Do not suppress errors on `agent-browser` commands. Remove `|| true`, `|| :`, `2>/dev/null`, `; true`, and similar redirects so ccqa can detect failures and roll back unsuccessful attempts. Run the command standalone and let it surface its exit code.";
|
|
1073
|
+
const runProducedUrl = findRunProducedOpenUrl(cmd, envScrubMap);
|
|
1074
|
+
if (runProducedUrl !== null) return `Do not open ${runProducedUrl} — that address was produced by this run (the id in it belongs to a record this run created), so the generated test would open a record later runs do not have. Reach the page the way a person does: click through from where the run already is. If the spec's instruction really names this exact address, put the id in a profile variable so it survives replay.`;
|
|
1075
|
+
return null;
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* What recording this command produces, and — for a marked probe — what its
|
|
1079
|
+
* output must say for the check to hold. Null when it records nothing.
|
|
1080
|
+
*/
|
|
1081
|
+
function abActionEventFromCommand(cmd) {
|
|
1082
|
+
const assertMarker = extractCcqaAssertFromBashCommand(cmd);
|
|
1083
|
+
const ab = extractAbActionFromBashCommand(cmd) ?? (assertMarker !== null ? extractObservationAbAction(cmd) : null);
|
|
1084
|
+
if (ab === null && assertMarker === null) return null;
|
|
1085
|
+
const stepId = extractCcqaStepFromBashCommand(cmd);
|
|
1086
|
+
return {
|
|
1087
|
+
event: {
|
|
1088
|
+
...ab !== null ? { abAction: ab } : {},
|
|
1089
|
+
...stepId ? { stepId } : {},
|
|
1090
|
+
...assertMarker !== null ? { assertMarker } : {},
|
|
1091
|
+
...hasCcqaSecretPrefix(cmd) ? { secret: true } : {}
|
|
1092
|
+
},
|
|
1093
|
+
holds: assertMarker === null ? null : markerHolds(assertMarker, ab)
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1078
1096
|
//#endregion
|
|
1079
1097
|
//#region src/drift/auth.ts
|
|
1080
1098
|
/**
|