Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 3m58s
Applied diagnostic surface contract rules to Audit Log inspect modal and Support Diagnostics action context, consolidating raw diagnostic data into safe modals according to Spec 374.
515 lines
21 KiB
PHP
515 lines
21 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\AuditLog;
|
|
use App\Models\EvidenceSnapshot;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\OperationRun;
|
|
use App\Models\ReviewPack;
|
|
use App\Models\StoredReport;
|
|
use App\Models\User;
|
|
use App\Support\Evidence\EvidenceCompletenessState;
|
|
use App\Support\Evidence\EvidenceSnapshotStatus;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\ReviewPackStatus;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
|
|
pest()->browser()->timeout(60_000);
|
|
|
|
it('Spec329 smokes Evidence Overview proof-first disclosure and filter clearing', function (): void {
|
|
$fixture = spec329BrowserDisclosureFixture();
|
|
spec329AuthenticateDisclosureBrowser($this, $fixture['user'], $fixture['environmentA']);
|
|
|
|
$cleanPath = json_encode((string) parse_url(route('admin.evidence.overview'), PHP_URL_PATH), JSON_THROW_ON_ERROR);
|
|
|
|
$page = visit(route('admin.evidence.overview'))
|
|
->resize(1440, 1100)
|
|
->waitForText('What proof is available for this scope?')
|
|
->assertDontSee(__('localization.shell.no_environment_selected'))
|
|
->assertDontSee('Environment filter:')
|
|
->assertSee('Evidence proof workbench')
|
|
->assertSee('Primary proof path')
|
|
->assertSee('Evidence path')
|
|
->assertSee('Evidence snapshot')
|
|
->assertSee('Review pack')
|
|
->assertSee('Stored report / export')
|
|
->assertSee('Operation proof')
|
|
->assertSee('Proof incomplete')
|
|
->assertSee('A proof record exists, but no usable captured evidence is available yet.')
|
|
->assertSee('Primary evidence snapshot is empty.')
|
|
->assertSee('Supporting proof exists through the review pack, stored report, and operation record.')
|
|
->assertSee('Empty')
|
|
->assertSee('Ready')
|
|
->assertSee('Available')
|
|
->assertSourceHas('Search evidence or next step')
|
|
->assertSee('Evidence inventory')
|
|
->assertSee($fixture['environmentA']->name)
|
|
->assertSee($fixture['environmentB']->name)
|
|
->assertDontSee('The artifact row exists, but it does not contain usable captured content.')
|
|
->assertDontSee('artifact row exists')
|
|
->assertSourceMissing('Search tenant or next')
|
|
->assertDontSee('Empty...')
|
|
->assertDontSee('Re...')
|
|
->assertDontSee('raw payload should stay hidden')
|
|
->assertDontSee('provider secret should stay hidden')
|
|
->assertDontSee('stack trace should stay hidden')
|
|
->assertDontSee('debug metadata should stay hidden')
|
|
->assertDontSee('internal exception should stay hidden')
|
|
->assertDontSee('current tenant')
|
|
->assertDontSee('tenant filter')
|
|
->assertDontSee('entitled tenant')
|
|
->assertDontSee('all tenants')
|
|
->assertScript('document.querySelector("[data-testid=\"evidence-disclosure-diagnostics\"]")?.open === false', true)
|
|
->assertScript('(() => {
|
|
const action = document.querySelector("[data-testid=\"evidence-primary-proof-action\"]");
|
|
|
|
if (! action) {
|
|
return false;
|
|
}
|
|
|
|
const box = action.getBoundingClientRect();
|
|
|
|
return action.textContent.trim() === "Open evidence snapshot"
|
|
&& box.height > 0
|
|
&& box.height <= 44
|
|
&& action.scrollWidth <= Math.ceil(action.clientWidth) + 1
|
|
&& getComputedStyle(action).whiteSpace === "nowrap";
|
|
})()', true)
|
|
->assertScript('(() => {
|
|
const badges = Array.from(document.querySelectorAll("[data-testid=\"evidence-path-state-badge\"]"));
|
|
|
|
return badges.length >= 4
|
|
&& badges.every((badge) => {
|
|
const label = badge.textContent.trim();
|
|
const box = badge.getBoundingClientRect();
|
|
|
|
return ! ["Empty...", "Re..."].includes(label)
|
|
&& box.width > 0
|
|
&& badge.scrollWidth <= Math.ceil(badge.clientWidth) + 1;
|
|
});
|
|
})()', true)
|
|
->assertScript('(() => {
|
|
const grid = document.querySelector("[data-testid=\"evidence-disclosure-workbench\"]");
|
|
const main = document.querySelector("[data-testid=\"evidence-proof-primary\"]");
|
|
const aside = document.querySelector("[data-testid=\"evidence-proof-aside\"]");
|
|
|
|
if (! grid || ! main || ! aside) {
|
|
return false;
|
|
}
|
|
|
|
const children = Array.from(grid.children);
|
|
const mainBox = main.getBoundingClientRect();
|
|
const asideBox = aside.getBoundingClientRect();
|
|
|
|
return window.innerWidth >= 1024
|
|
&& grid.classList.contains("lg:grid-cols-[minmax(0,1fr)_22rem]")
|
|
&& aside.tagName === "ASIDE"
|
|
&& children.indexOf(main) !== -1
|
|
&& children.indexOf(aside) > children.indexOf(main)
|
|
&& asideBox.left > mainBox.right
|
|
&& Math.abs(asideBox.top - mainBox.top) <= 8;
|
|
})()', true)
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->screenshot(true, spec329DisclosureScreenshot('evidence-overview--clean'));
|
|
|
|
spec329CopyDisclosureScreenshot('evidence-overview--clean');
|
|
|
|
$page = visit(route('admin.evidence.overview', [
|
|
'environment_id' => (int) $fixture['environmentA']->getKey(),
|
|
]))
|
|
->waitForText('Environment filter:')
|
|
->assertSee('Environment filter: '.$fixture['environmentA']->name)
|
|
->assertSee('What proof is available for this scope?')
|
|
->assertSee($fixture['environmentA']->name)
|
|
->assertSee('Proof incomplete')
|
|
->assertSee('Primary evidence snapshot is empty.')
|
|
->assertDontSee('The artifact row exists, but it does not contain usable captured content.')
|
|
->assertDontSee('Empty...')
|
|
->assertDontSee('Re...')
|
|
->assertDontSee($fixture['environmentB']->name)
|
|
->assertScript('document.querySelector("[data-testid=\"evidence-disclosure-diagnostics\"]")?.open === false', true)
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->screenshot(true, spec329DisclosureScreenshot('evidence-overview--filtered'));
|
|
|
|
spec329CopyDisclosureScreenshot('evidence-overview--filtered');
|
|
|
|
spec329ClearDisclosureEnvironmentFilter($page)
|
|
->waitForText($fixture['environmentB']->name)
|
|
->assertDontSee('Environment filter:')
|
|
->waitForText($fixture['environmentB']->name)
|
|
->assertScript("window.location.pathname === {$cleanPath}", true)
|
|
->assertScript('! window.location.search.includes("environment_id=")', true)
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->screenshot(true, spec329DisclosureScreenshot('evidence-overview--after-clear'));
|
|
|
|
spec329CopyDisclosureScreenshot('evidence-overview--after-clear');
|
|
|
|
$page->script('window.location.reload();');
|
|
|
|
$page
|
|
->waitForText($fixture['environmentB']->name)
|
|
->assertDontSee('Environment filter:')
|
|
->assertSee($fixture['environmentB']->name)
|
|
->assertScript("window.location.pathname === {$cleanPath}", true)
|
|
->assertScript('! window.location.search.includes("environment_id=")', true)
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->screenshot(true, spec329DisclosureScreenshot('evidence-overview--after-reload'));
|
|
|
|
spec329CopyDisclosureScreenshot('evidence-overview--after-reload');
|
|
});
|
|
|
|
it('Spec329 smokes Audit Log event-proof disclosure and filter clearing', function (): void {
|
|
$fixture = spec329BrowserDisclosureFixture();
|
|
spec329AuthenticateDisclosureBrowser($this, $fixture['user'], $fixture['environmentA']);
|
|
|
|
$cleanPath = json_encode((string) parse_url(route('admin.monitoring.audit-log'), PHP_URL_PATH), JSON_THROW_ON_ERROR);
|
|
|
|
$page = visit(route('admin.monitoring.audit-log', [
|
|
'event' => (int) $fixture['auditA']->getKey(),
|
|
]))
|
|
->resize(1440, 1100)
|
|
->waitForText('Which event proves what happened?')
|
|
->assertDontSee(__('localization.shell.no_environment_selected'))
|
|
->assertDontSee('Environment filter:')
|
|
->assertSee('Audit proof workbench')
|
|
->assertSee('Selected event proof')
|
|
->assertSee('Event proof')
|
|
->assertSee('Actor')
|
|
->assertSee('Action')
|
|
->assertSee('Target')
|
|
->assertSee('Outcome')
|
|
->assertSee('Time')
|
|
->assertSee('Related proof')
|
|
->assertSee('Operation proof')
|
|
->assertSee('Readable context')
|
|
->assertSee('Audit event history')
|
|
->assertSee('Spec329 Browser Operator A')
|
|
->assertSee('Permission posture checked')
|
|
->assertSee('Permission posture report')
|
|
->assertDontSee('raw payload should stay hidden')
|
|
->assertDontSee('provider secret should stay hidden')
|
|
->assertDontSee('stack trace should stay hidden')
|
|
->assertDontSee('debug metadata should stay hidden')
|
|
->assertDontSee('internal exception should stay hidden')
|
|
->assertDontSee('provider response should stay hidden')
|
|
->assertDontSee('current tenant')
|
|
->assertDontSee('tenant filter')
|
|
->assertDontSee('entitled tenant')
|
|
->assertDontSee('all tenants')
|
|
->assertScript('document.querySelector("[data-testid=\"audit-disclosure-diagnostics\"]")?.open === false', true)
|
|
->assertScript('document.querySelector("[data-testid=\"audit-event-diagnostics\"]")?.open === false', true)
|
|
->assertScript('(() => {
|
|
const selectedGrid = document.querySelector("[data-testid=\"audit-selected-proof-cards\"]");
|
|
|
|
if (! selectedGrid) {
|
|
return false;
|
|
}
|
|
|
|
const cards = Array.from(selectedGrid.children);
|
|
|
|
return selectedGrid.classList.contains("sm:grid-cols-2")
|
|
&& selectedGrid.classList.contains("xl:grid-cols-3")
|
|
&& selectedGrid.classList.contains("2xl:grid-cols-5")
|
|
&& cards.length >= 5
|
|
&& cards.every((card) => getComputedStyle(card).minWidth === "0px");
|
|
})()', true)
|
|
->assertScript('(() => {
|
|
const selectedGrid = document.querySelector("[data-testid=\"audit-selected-proof-cards\"]");
|
|
|
|
if (! selectedGrid) {
|
|
return false;
|
|
}
|
|
|
|
const wrappedNodes = Array.from(selectedGrid.querySelectorAll(".wrap-anywhere"));
|
|
const cards = Array.from(selectedGrid.children);
|
|
|
|
return wrappedNodes.length >= 8
|
|
&& wrappedNodes.every((node) => node.closest(".rounded-2xl") instanceof HTMLElement)
|
|
&& cards.every((card) => card.scrollWidth <= Math.ceil(card.clientWidth) + 1);
|
|
})()', true)
|
|
->assertScript('(() => {
|
|
const grid = document.querySelector("[data-testid=\"audit-disclosure-workbench\"]");
|
|
const main = document.querySelector("[data-testid=\"audit-proof-primary\"]");
|
|
const aside = document.querySelector("[data-testid=\"audit-proof-aside\"]");
|
|
|
|
if (! grid || ! main || ! aside) {
|
|
return false;
|
|
}
|
|
|
|
const children = Array.from(grid.children);
|
|
const mainBox = main.getBoundingClientRect();
|
|
const asideBox = aside.getBoundingClientRect();
|
|
|
|
return window.innerWidth >= 1024
|
|
&& grid.classList.contains("lg:grid-cols-[minmax(0,1fr)_22rem]")
|
|
&& aside.tagName === "ASIDE"
|
|
&& children.indexOf(main) !== -1
|
|
&& children.indexOf(aside) > children.indexOf(main)
|
|
&& asideBox.left > mainBox.right
|
|
&& Math.abs(asideBox.top - mainBox.top) <= 8;
|
|
})()', true)
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->screenshot(true, spec329DisclosureScreenshot('audit-log--clean'));
|
|
|
|
spec329CopyDisclosureScreenshot('audit-log--clean');
|
|
|
|
$page = visit(route('admin.monitoring.audit-log', [
|
|
'environment_id' => (int) $fixture['environmentA']->getKey(),
|
|
'event' => (int) $fixture['auditA']->getKey(),
|
|
]))
|
|
->waitForText('Environment filter:')
|
|
->assertSee('Environment filter: '.$fixture['environmentA']->name)
|
|
->assertSee('Which event proves what happened?')
|
|
->assertSee('Permission posture checked')
|
|
->assertDontSee('Workspace selected by browser proof B')
|
|
->assertScript('document.querySelector("[data-testid=\"audit-disclosure-diagnostics\"]")?.open === false', true)
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->screenshot(true, spec329DisclosureScreenshot('audit-log--filtered'));
|
|
|
|
spec329CopyDisclosureScreenshot('audit-log--filtered');
|
|
|
|
spec329ClearDisclosureEnvironmentFilter($page)
|
|
->waitForText('Workspace selected by browser proof B')
|
|
->assertDontSee('Environment filter:')
|
|
->waitForText('Workspace selected by browser proof B')
|
|
->assertScript("window.location.pathname === {$cleanPath}", true)
|
|
->assertScript('! window.location.search.includes("environment_id=")', true)
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->screenshot(true, spec329DisclosureScreenshot('audit-log--after-clear'));
|
|
|
|
spec329CopyDisclosureScreenshot('audit-log--after-clear');
|
|
|
|
$page->script('window.location.reload();');
|
|
|
|
$page
|
|
->waitForText('Workspace selected by browser proof B')
|
|
->assertDontSee('Environment filter:')
|
|
->assertSee('Workspace selected by browser proof B')
|
|
->assertScript("window.location.pathname === {$cleanPath}", true)
|
|
->assertScript('! window.location.search.includes("environment_id=")', true)
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->screenshot(true, spec329DisclosureScreenshot('audit-log--after-reload'));
|
|
|
|
spec329CopyDisclosureScreenshot('audit-log--after-reload');
|
|
});
|
|
|
|
/**
|
|
* @return array{
|
|
* user: User,
|
|
* environmentA: ManagedEnvironment,
|
|
* environmentB: ManagedEnvironment,
|
|
* auditA: AuditLog,
|
|
* auditB: AuditLog
|
|
* }
|
|
*/
|
|
function spec329BrowserDisclosureFixture(): array
|
|
{
|
|
$environmentA = ManagedEnvironment::factory()->active()->create([
|
|
'name' => 'Spec329 Browser Tenant Environment A',
|
|
'external_id' => 'spec329-browser-environment-a',
|
|
]);
|
|
|
|
[$user, $environmentA] = createUserWithTenant(
|
|
tenant: $environmentA,
|
|
role: 'owner',
|
|
workspaceRole: 'owner',
|
|
);
|
|
|
|
$environmentB = ManagedEnvironment::factory()->active()->create([
|
|
'workspace_id' => (int) $environmentA->workspace_id,
|
|
'name' => 'Spec329 Browser Environment B',
|
|
'external_id' => 'spec329-browser-environment-b',
|
|
]);
|
|
|
|
createUserWithTenant(
|
|
tenant: $environmentB,
|
|
user: $user,
|
|
role: 'owner',
|
|
workspaceRole: 'owner',
|
|
);
|
|
|
|
$runA = OperationRun::factory()->forTenant($environmentA)->create([
|
|
'type' => 'tenant.evidence.snapshot.generate',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
|
'context' => [
|
|
'raw_payload' => 'raw payload should stay hidden',
|
|
'provider_secret' => 'provider secret should stay hidden',
|
|
'stack_trace' => 'stack trace should stay hidden',
|
|
'debug_metadata' => 'debug metadata should stay hidden',
|
|
'internal_exception' => 'internal exception should stay hidden',
|
|
],
|
|
'completed_at' => now()->subMinutes(10),
|
|
]);
|
|
|
|
$runB = OperationRun::factory()->forTenant($environmentB)->create([
|
|
'type' => 'policy.sync',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
|
'completed_at' => now()->subMinutes(5),
|
|
]);
|
|
|
|
$snapshotA = EvidenceSnapshot::query()->create([
|
|
'managed_environment_id' => (int) $environmentA->getKey(),
|
|
'workspace_id' => (int) $environmentA->workspace_id,
|
|
'operation_run_id' => (int) $runA->getKey(),
|
|
'status' => EvidenceSnapshotStatus::Active->value,
|
|
'completeness_state' => EvidenceCompletenessState::Complete->value,
|
|
'summary' => [
|
|
'missing_dimensions' => 0,
|
|
'stale_dimensions' => 0,
|
|
'raw_payload' => 'raw payload should stay hidden',
|
|
],
|
|
'generated_at' => now()->subMinutes(4),
|
|
'expires_at' => now()->addDays(30),
|
|
]);
|
|
|
|
EvidenceSnapshot::query()->create([
|
|
'managed_environment_id' => (int) $environmentB->getKey(),
|
|
'workspace_id' => (int) $environmentB->workspace_id,
|
|
'operation_run_id' => (int) $runB->getKey(),
|
|
'status' => EvidenceSnapshotStatus::Active->value,
|
|
'completeness_state' => EvidenceCompletenessState::Partial->value,
|
|
'summary' => [
|
|
'missing_dimensions' => 1,
|
|
'stale_dimensions' => 0,
|
|
],
|
|
'generated_at' => now()->subMinutes(9),
|
|
'expires_at' => now()->addDays(30),
|
|
]);
|
|
|
|
ReviewPack::factory()->ready()->create([
|
|
'managed_environment_id' => (int) $environmentA->getKey(),
|
|
'workspace_id' => (int) $environmentA->workspace_id,
|
|
'evidence_snapshot_id' => (int) $snapshotA->getKey(),
|
|
'operation_run_id' => (int) $runA->getKey(),
|
|
'status' => ReviewPackStatus::Ready->value,
|
|
]);
|
|
|
|
$storedReport = StoredReport::factory()->permissionPosture([
|
|
'raw_payload' => 'raw payload should stay hidden',
|
|
])->create([
|
|
'managed_environment_id' => (int) $environmentA->getKey(),
|
|
'workspace_id' => (int) $environmentA->workspace_id,
|
|
'fingerprint' => 'spec329-browser-report',
|
|
]);
|
|
|
|
$auditA = AuditLog::query()->create([
|
|
'workspace_id' => (int) $environmentA->workspace_id,
|
|
'managed_environment_id' => (int) $environmentA->getKey(),
|
|
'operation_run_id' => (int) $runA->getKey(),
|
|
'actor_email' => 'spec329-browser-operator-with-a-very-long-address+wrap-proof@example.test',
|
|
'actor_name' => 'Spec329 Browser Operator A',
|
|
'actor_type' => 'human',
|
|
'action' => 'permission_posture.checked',
|
|
'status' => 'success',
|
|
'resource_type' => 'stored_report',
|
|
'resource_id' => (string) $storedReport->getKey(),
|
|
'target_label' => 'Permission posture report',
|
|
'summary' => 'Permission posture checked',
|
|
'metadata' => [
|
|
'reason' => 'Evidence review',
|
|
'raw_payload' => 'raw payload should stay hidden',
|
|
'provider_secret' => 'provider secret should stay hidden',
|
|
'stack_trace' => 'stack trace should stay hidden',
|
|
'debug_metadata' => 'debug metadata should stay hidden',
|
|
'internal_exception' => 'internal exception should stay hidden',
|
|
'provider_response' => 'provider response should stay hidden',
|
|
],
|
|
'recorded_at' => now()->subMinutes(2),
|
|
]);
|
|
|
|
$auditB = AuditLog::query()->create([
|
|
'workspace_id' => (int) $environmentB->workspace_id,
|
|
'managed_environment_id' => (int) $environmentB->getKey(),
|
|
'actor_email' => 'spec329-b@example.test',
|
|
'actor_name' => 'Spec329 Browser Operator B',
|
|
'actor_type' => 'human',
|
|
'action' => 'workspace.selected',
|
|
'status' => 'success',
|
|
'resource_type' => 'workspace',
|
|
'resource_id' => (string) $environmentB->workspace_id,
|
|
'target_label' => 'Workspace '.$environmentB->workspace_id,
|
|
'summary' => 'Workspace selected by browser proof B',
|
|
'recorded_at' => now()->subMinute(),
|
|
]);
|
|
|
|
return [
|
|
'user' => $user,
|
|
'environmentA' => $environmentA,
|
|
'environmentB' => $environmentB,
|
|
'auditA' => $auditA,
|
|
'auditB' => $auditB,
|
|
];
|
|
}
|
|
|
|
function spec329AuthenticateDisclosureBrowser(
|
|
mixed $test,
|
|
User $user,
|
|
ManagedEnvironment $rememberedEnvironment,
|
|
): void {
|
|
$workspaceId = (int) $rememberedEnvironment->workspace_id;
|
|
|
|
$session = [
|
|
WorkspaceContext::SESSION_KEY => $workspaceId,
|
|
WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [
|
|
(string) $workspaceId => (int) $rememberedEnvironment->getKey(),
|
|
],
|
|
];
|
|
|
|
$test->actingAs($user)->withSession($session);
|
|
|
|
foreach ($session as $key => $value) {
|
|
session()->put($key, $value);
|
|
}
|
|
|
|
setAdminPanelContext($rememberedEnvironment);
|
|
}
|
|
|
|
function spec329ClearDisclosureEnvironmentFilter(mixed $page): mixed
|
|
{
|
|
$page->assertScript('document.querySelector(\'[data-testid="workspace-hub-environment-filter-clear"]\') instanceof HTMLAnchorElement', true);
|
|
$page->script('window.location.assign(document.querySelector(\'[data-testid="workspace-hub-environment-filter-clear"]\').href);');
|
|
|
|
return $page;
|
|
}
|
|
|
|
function spec329DisclosureScreenshot(string $name): string
|
|
{
|
|
return 'spec329-'.$name;
|
|
}
|
|
|
|
function spec329CopyDisclosureScreenshot(string $name, ?string $targetFilename = null): void
|
|
{
|
|
$filename = spec329DisclosureScreenshot($name).'.png';
|
|
$source = base_path('tests/Browser/Screenshots/'.$filename);
|
|
$targetDirectory = repo_path('specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots');
|
|
$targetFilename ??= $filename;
|
|
|
|
if (! is_dir($targetDirectory)) {
|
|
@mkdir($targetDirectory, 0755, true);
|
|
}
|
|
|
|
if (! is_dir($targetDirectory) || ! is_writable($targetDirectory)) {
|
|
return;
|
|
}
|
|
|
|
if (! is_file($source)) {
|
|
$source = \Pest\Browser\Support\Screenshot::path($filename);
|
|
}
|
|
|
|
if (is_file($source)) {
|
|
@copy($source, $targetDirectory.DIRECTORY_SEPARATOR.$targetFilename);
|
|
}
|
|
}
|