Merge branch 'unstable' into ai/tests-improve-coverage-20250220-10675377090391221934

This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-30 09:27:07 -05:00
commit 9392e08cbd
29 files changed
+1377 -1034

No files matched your search

+35 -17
View File
@@ -1,27 +1,45 @@
# Developer Onboarding Audit Report
**Date:** 2023-10-24
**Auditor:** AI Agent
**Date:** 2026-01-30
**Agent:** Jules
## Summary
The onboarding process was audited by simulating a fresh checkout and executing the documented steps. All commands succeeded without errors. The documentation was updated to reflect a more streamlined workflow using `npm start`.
## Findings
The onboarding process was audited by simulating a fresh developer checkout. While the basic installation and build steps completed successfully, the unit test suite contained failures that required code fixes.
### 1. Command Execution
The following commands were executed from a clean state (simulated):
- `npm ci`: **Success** (Installed 314 packages)
- `npm run build:css`: **Success** (Generated `css/tailwind.generated.css`)
- `npm run format`: **Success** (Formatted files)
## Steps Executed
### 2. Documentation
- **Observation:** `README.md` and `CONTRIBUTING.md` had slightly diverging instructions for building and starting the server. `README.md` suggested manual build then `python` or `npx serve`. `CONTRIBUTING.md` focused on `npm run build`.
- **Action:** Both documents were updated to recommend `npm start` as the primary command, which handles both building and serving. This simplifies the onboarding process.
1. **Dependencies**: `npm ci` (Success)
2. **Build**: `npm run build:css` (Success, with warning)
3. **Format**: `npm run format` (Success)
4. **Lint**: `npm run lint` (Success)
5. **Tests**: `npm run test:unit` (Failure initially)
### 3. Dev Container
- **Observation:** The project has a valid `.devcontainer/devcontainer.json`.
- **Action:** Added `"forwardPorts": [3000]` to the configuration to automatically expose the development server port when running in a container.
## Findings & Fixes
### 1. Unit Test Failure: `tests/app-batch-fetch-profiles.test.mjs`
* **Issue**: The test failed with `expected a query per relay. 4 !== 2`.
* **Cause**: The test mocked `nostrClient.relays` and `writeRelays` but not `readRelays`. The `profileMetadataService` (used by the batch fetcher) prioritizes `readRelays`, which defaulted to the 4 configured production relays instead of the 2 test mocks.
* **Fix**: Updated the test to explicitly mock `nostrClient.readRelays` to match the test configuration.
### 2. Unit Test Failure: `tests/profile-modal-controller.test.mjs`
* **Issue**: The test failed with `TypeError: this.hashtagPreferencesService.load is not a function`.
* **Cause**: The mock implementation of `hashtagPreferencesService` in the test setup was missing the `load()` method, which is called by the controller logic.
* **Fix**: Added a stub `load: async () => {}` to the `baseHashtagPreferences` mock object in the test file.
### 3. Build Warning: `caniuse-lite is outdated`
* **Issue**: `npm run build:css` outputs a warning: `Browserslist: caniuse-lite is outdated`.
* **Action**: Executed `npx update-browserslist-db@latest`.
* **Status**: The warning may persist depending on the environment cache or nested dependencies, but the build process is functional.
## Recommendations
- Use `npm start` for local development.
- Use the Dev Container for a guaranteed reproducible environment if local setup issues arise.
* **Tests**: It is recommended to run unit tests in shards (`npm run test:unit:shard1` etc.) locally if the full suite times out, as observed in this environment.
* **Documentation**: The `CONTRIBUTING.md` guide is generally accurate.
## Conclusion
The onboarding friction was primarily due to bit-rot in unit tests. These have been corrected. The environment is now ready for development.
+5 -2
View File
@@ -431,8 +431,11 @@ artifacts now that the deploy pipeline owns the build step.
preparing a new deployment.
- See [`docs/instance-config.md`](docs/instance-config.md) for a full
reference of every setting and how to tune it.
- Flip `IS_DEV_MODE` to `false` before shipping production builds. The flag
flows into `js/config.js` as `isDevMode`, seeds the
- `IS_DEV_MODE` and `IS_VERBOSE_DEV_MODE` default to `false` for production.
If you need dev logging in a staging or QA build without editing source,
inject `window.__BITVID_DEV_MODE_OVERRIDE__` and/or
`window.__BITVID_VERBOSE_DEV_MODE_OVERRIDE__` before `js/config.js` loads.
The flag flows into `js/config.js` as `isDevMode`, seeds the
`window.__BITVID_DEV_MODE__` global for inline scripts, and gates whether the
dev logging channel emits to the console. See
[`docs/logging.md`](docs/logging.md) for rollout guidance.
+9 -5
View File
@@ -17,11 +17,13 @@
* Whether the current deployment should enable developer-centric behavior.
*
* When `true`, bitvid surfaces extra logging, debug helpers, and experimental
* UI affordances intended for development environments. Set the value to
* `false` before deploying to production so that end users receive the stable
* experience. Downstream modules read this flag via `js/config.js`.
* UI affordances intended for development environments. Leave the value
* `false` for production so end users receive the stable experience; use the
* runtime overrides documented in `js/config.js` when you need to toggle dev
* mode without editing this file. Downstream modules read this flag via
* `js/config.js`.
*/
export const IS_DEV_MODE = true;
export const IS_DEV_MODE = false;
/**
* Whether development builds should surface extra verbose diagnostics.
@@ -29,8 +31,10 @@ export const IS_DEV_MODE = true;
* Set this to `true` to keep high-volume development warnings (like COUNT
* request failures) visible in the console. Flip it to `false` when you want
* a quieter dev mode without disabling other development helpers entirely.
* Use the runtime override documented in `js/config.js` if you need to toggle
* verbosity without editing this file.
*/
export const IS_VERBOSE_DEV_MODE = true;
export const IS_VERBOSE_DEV_MODE = false;
/**
* Whether the current deployment should operate in lockdown mode.
+18 -2
View File
@@ -28,14 +28,30 @@ an operator might change it.
debug affordances. The value is re-exported as `isDevMode` in
`js/config.js` and controls `window.__BITVID_DEV_MODE__` as well as
logger routing.
- **Why change it:** Set to `false` for production builds so only the stable
UX and user-facing logging remain.
- **Why change it:** Defaults to `false` for production builds. Set to `true`
for local development, or use the runtime override described below to
toggle dev mode without editing this file.
- **`IS_VERBOSE_DEV_MODE`**
- **What it does:** Enables high-volume diagnostics inside dev mode (for
example, repeated COUNT failures).
- **Why change it:** Keep `true` while debugging noisy workflows and set to
`false` when you want a quieter dev console without disabling dev mode.
Defaults to `false` for production.
To override the dev flags at runtime (for example in a staging build) without
changing `config/instance-config.js`, inject one or both of the following
globals **before** `js/config.js` loads:
```html
<script>
window.__BITVID_DEV_MODE_OVERRIDE__ = true;
window.__BITVID_VERBOSE_DEV_MODE_OVERRIDE__ = true;
</script>
```
The overrides accept boolean values or the strings `"true"` / `"false"` so they
can be injected at build time.
- **`IS_LOCKDOWN_MODE`**
- **What it does:** Toggles a lockdown posture that limits access and switches
+16 -3
View File
@@ -59,10 +59,23 @@ The source of truth lives in `config/instance-config.js`:
- Set `IS_DEV_MODE = true` while developing locally so the dev logger and other
diagnostics are available.
- Flip `IS_DEV_MODE = false` before shipping a production build. This change
propagates to `isDevMode` (the module export), `window.__BITVID_DEV_MODE__`,
and `logger.dev`, silencing development-only messages.
- Keep `IS_DEV_MODE = false` for production builds. This value propagates to
`isDevMode` (the module export), `window.__BITVID_DEV_MODE__`, and
`logger.dev`, silencing development-only messages.
Always commit the flag change alongside deployment-ready configuration tweaks.
Forgetting to update the flag leaves verbose traces in user consoles and can
expose experimental behavior intended only for QA environments.
If you need to enable dev logging without editing `config/instance-config.js`,
inject a runtime override before `js/config.js` loads:
```html
<script>
window.__BITVID_DEV_MODE_OVERRIDE__ = true;
window.__BITVID_VERBOSE_DEV_MODE_OVERRIDE__ = true;
</script>
```
Overrides accept boolean values or the strings `"true"` / `"false"` so they can
be injected by a build pipeline.
+14 -4
View File
@@ -1,7 +1,17 @@
# Upgrade cross-env
- **Current Version:** 7.0.3
- **Latest Version:** 10.1.0
## Status
- **Current:** `7.0.3`
- **Latest:** `10.1.0`
**Status:** Major Upgrade Required
**Reason:** Major version bump available. v10 requires Node >= 20, which may break CI/CD or local environments running older Node versions (e.g. Node 18). Requires explicit environment verification and separate PR.
## Details
Major version upgrade.
Used in npm scripts to set environment variables.
## Plan
1. Update `cross-env`.
2. Check if node version requirements changed (v10+ required likely).
3. Run all npm scripts to verify `cross-env` invocations work.
## Guardrails
- Major upgrade.
+14 -4
View File
@@ -1,7 +1,17 @@
# Upgrade esbuild
- **Current Version:** 0.25.12
- **Latest Version:** 0.27.2
## Status
- **Current:** `0.25.12`
- **Latest:** `0.27.2`
**Status:** Major Upgrade Required
**Reason:** 0.x version bump may contain breaking changes. Requires strict testing and separate PR.
## Details
`esbuild` 0.x versions may have breaking changes between minor versions.
Used for build scripts (`scripts/build-dist.mjs`, `scripts/build-beacon.mjs`).
## Plan
1. Update `esbuild`.
2. Run build scripts: `npm run build`, `npm run build:beacon`.
3. Verify output bundles work correctly.
## Guardrails
- Build tool upgrade.
+18 -5
View File
@@ -1,8 +1,21 @@
# Upgrade nostr-tools
- **Current Version:** 2.19.4
- **Wanted Version:** 2.20.0
- **Latest Version:** 2.20.0
## Status
- **Current (Package):** `^2.19.4`
- **Current (Vendor):** `2.17.0` (found in `vendor/nostr-tools.bundle.min.js`)
- **Latest:** `2.21.0`
**Status:** Skipped (Risky/Protocol)
**Reason:** This is a cryptographic/protocol library. As per security guardrails, manual review is required before upgrading.
## Details
`nostr-tools` is a core protocol library.
There is a discrepancy between `package.json` (2.19.4) and the vendored bundle (2.17.0).
The latest version 2.21.0 is available.
## Plan
1. Review changelogs for breaking changes between 2.17.0/2.19.4 and 2.21.0.
2. Update `package.json` to `2.21.0`.
3. Update `vendor/nostr-tools.bundle.min.js` to match the new version.
4. Verify `js/nostr/client.js` and other consumers work correctly.
5. Run full E2E and Unit tests.
## Guardrails
- Protocol library: Requires manual review.
+14 -4
View File
@@ -1,7 +1,17 @@
# Upgrade pixelmatch
- **Current Version:** 5.3.0
- **Latest Version:** 7.1.0
## Status
- **Current:** `5.3.0`
- **Latest:** `7.1.0`
**Status:** Major Upgrade Required
**Reason:** Major version bump available. Requires strict testing and separate PR.
## Details
Major version upgrade.
Used in visual tests.
## Plan
1. Update `pixelmatch`.
2. Run `npm run test:visual`.
3. Check if image comparison logic needs adjustment (sensitivity, anti-aliasing handling).
## Guardrails
- Major upgrade.
+14 -4
View File
@@ -1,7 +1,17 @@
# Upgrade prettier-plugin-tailwindcss
- **Current Version:** 0.6.14
- **Latest Version:** 0.7.2
## Status
- **Current:** `0.6.14`
- **Latest:** `0.7.2`
**Status:** Major Upgrade Required
**Reason:** 0.x version bump may contain breaking changes. Requires strict testing and separate PR.
## Details
0.x minor version upgrade, potential breaking changes.
Depends on `prettier` and `tailwindcss` versions.
## Plan
1. Update `prettier-plugin-tailwindcss`.
2. Run `npm run format`.
3. Check for formatting changes.
## Guardrails
- Dev tool upgrade.
+14 -4
View File
@@ -1,7 +1,17 @@
# Upgrade stylelint
- **Current Version:** 16.26.1
- **Latest Version:** 17.0.0
## Status
- **Current:** `16.26.1`
- **Latest:** `17.0.0`
**Status:** Major Upgrade Required
**Reason:** Major version bump available. Requires strict testing and separate PR.
## Details
Major version upgrade.
May require updating rules in `stylelint.config.cjs`.
## Plan
1. Update `stylelint`.
2. Run `npm run lint:css` to identify config errors or new violations.
3. Fix config/violations.
## Guardrails
- Major upgrade.
+16 -4
View File
@@ -1,7 +1,19 @@
# Upgrade tailwindcss
- **Current Version:** 3.4.19
- **Latest Version:** 4.1.18
## Status
- **Current:** `3.4.19`
- **Latest:** `4.1.18`
**Status:** Major Upgrade Required
**Reason:** Major version bump available (v4). Requires strict testing, migration guide adherence, and separate PR.
## Details
Major version upgrade from v3 to v4.
This likely includes breaking changes in configuration, class names, or build process.
## Plan
1. Review Tailwind CSS v4 migration guide.
2. Update `tailwindcss`, `postcss`, `autoprefixer`.
3. Check `tailwind.config.cjs` compatibility.
4. Run `npm run build:css` and verify output.
5. Visual regression testing (`npm run test:visual`).
## Guardrails
- Major upgrade: Requires separate PR and thorough testing.
+19
View File
@@ -0,0 +1,19 @@
# Upgrade webtorrent
## Status
- **Current:** `2.5.11` (vendored in `js/webtorrent.min.js`)
- **Latest:** `2.8.5`
## Details
`webtorrent` is a core protocol library for video playback.
The current version is vendored and outdated.
## Plan
1. Review changelogs for breaking changes between 2.5.11 and 2.8.5.
2. Download new release or build from source.
3. Replace `js/webtorrent.min.js`.
4. Verify playback and fallbacks.
5. Smoke test Upload Modal and Magnet parsing.
## Guardrails
- Protocol library: Requires manual review.
+25 -148
View File
@@ -173,6 +173,7 @@ import EngagementController from "./ui/engagementController.js";
import SimilarContentController from "./ui/similarContentController.js";
import UrlHealthController from "./ui/urlHealthController.js";
import VideoModalCommentController from "./ui/videoModalCommentController.js";
import VideoModalController from "./ui/videoModalController.js";
import TorrentStatusController from "./ui/torrentStatusController.js";
import ShareNostrController from "./ui/shareNostrController.js";
import ModerationActionController from "./services/moderationActionController.js";
@@ -223,6 +224,14 @@ class Application {
setStoredCurrentVideo(value ?? null);
}
get activeProfilePubkey() {
return getActiveProfilePubkey();
}
get savedProfiles() {
return getSavedProfiles();
}
constructor({ services = {}, ui = {}, helpers = {}, loadView: viewLoader } = {}) {
this.loadView = typeof viewLoader === "function" ? viewLoader : null;
@@ -251,6 +260,16 @@ class Application {
const { modalManager } = this.bootstrapper.initialize();
this.modalManager = modalManager;
this.videoModalController = new VideoModalController({
getVideoModal: () => this.videoModal,
callbacks: {
showError: (msg) => this.showError(msg),
getLastModalTrigger: () => this.lastModalTrigger,
setLastModalTrigger: (val) => this.setLastModalTrigger(val),
getCurrentVideo: () => this.currentVideo,
},
});
this.modalCreatorProfileRequestToken = null;
this.dmRecipientPubkey = null;
this.dmRelayHints = new Map();
@@ -1176,162 +1195,21 @@ class Application {
* Show the modal and set the "Please stand by" poster on the video.
*/
async showModalWithPoster(video = this.currentVideo, options = {}) {
if (!this.videoModal) {
return null;
}
if (Object.prototype.hasOwnProperty.call(options || {}, "trigger")) {
this.setLastModalTrigger(options.trigger);
}
const targetVideo = video || this.currentVideo;
if (!targetVideo) {
this.log(
"[Application] Skipping video modal open; no target video is available.",
);
return null;
}
try {
const { root } = await this.ensureVideoModalReady({
ensureVideoElement: true,
});
if (!this.videoModal) {
return root || null;
}
this.videoModal.open(targetVideo, {
triggerElement: this.lastModalTrigger,
});
this.applyModalLoadingPoster();
return (
root ||
(typeof this.videoModal.getRoot === "function"
? this.videoModal.getRoot()
: null)
);
} catch (error) {
devLogger.error(
"[Application] Failed to open the video modal before playback:",
error
);
this.showError("Could not open the video player. Please try again.");
return null;
}
return this.videoModalController.showModalWithPoster(video, options);
}
applyModalLoadingPoster() {
if (!this.videoModal) {
return;
}
this.videoModal.applyLoadingPoster();
this.videoModalController.applyModalLoadingPoster();
}
forceRemoveModalPoster(reason = "manual-clear") {
if (!this.videoModal) {
return false;
}
return this.videoModal.forceRemovePoster(reason);
return this.videoModalController.forceRemoveModalPoster(reason);
}
async ensureVideoModalReady({ ensureVideoElement = false } = {}) {
if (!this.videoModal) {
throw new Error("Video modal instance is not available.");
}
const getRoot = () =>
typeof this.videoModal.getRoot === "function"
? this.videoModal.getRoot()
: null;
const getVideoElement = () =>
typeof this.videoModal.getVideoElement === "function"
? this.videoModal.getVideoElement()
: null;
const existingRoot = getRoot();
const existingVideoElement = getVideoElement();
const rootConnected = Boolean(existingRoot && existingRoot.isConnected);
const hasVideoElement = Boolean(existingVideoElement);
const videoConnected = Boolean(
existingVideoElement && existingVideoElement.isConnected,
);
const needsRehydrate =
!rootConnected || !hasVideoElement || !videoConnected;
if (!needsRehydrate) {
this.modalVideo = existingVideoElement;
if (
existingVideoElement &&
this.videoModal &&
typeof this.videoModal.setVideoElement === "function"
) {
const modalVideo =
typeof this.videoModal.getVideoElement === "function"
? this.videoModal.getVideoElement()
: null;
if (modalVideo !== existingVideoElement) {
this.videoModal.setVideoElement(existingVideoElement);
}
}
return {
root: existingRoot,
videoElement: existingVideoElement,
};
}
if (!videoConnected) {
this.modalVideo = null;
}
if (!this.videoModalReadyPromise) {
if (typeof this.videoModal.load !== "function") {
throw new Error("Video modal does not expose a load() method.");
}
this.videoModalReadyPromise = Promise.resolve(this.videoModal.load());
}
try {
await this.videoModalReadyPromise;
} catch (error) {
this.videoModalReadyPromise = null;
throw error;
}
this.videoModalReadyPromise = null;
const readyRoot = getRoot();
const readyVideoElement = getVideoElement();
const readyVideoConnected = Boolean(
readyVideoElement && readyVideoElement.isConnected,
);
if (readyVideoConnected) {
this.modalVideo = readyVideoElement;
if (
this.videoModal &&
typeof this.videoModal.setVideoElement === "function"
) {
this.videoModal.setVideoElement(readyVideoElement);
}
} else {
this.modalVideo = null;
}
if (ensureVideoElement && !readyVideoConnected) {
throw new Error(
"Video modal video element is missing after load().",
);
}
return {
root: readyRoot,
videoElement: readyVideoConnected ? readyVideoElement : null,
};
const result = await this.videoModalController.ensureVideoModalReady({ ensureVideoElement });
this.modalVideo = result.videoElement;
return result;
}
initializeSimilarContentController() {
@@ -10024,7 +9902,6 @@ class Application {
destroy() {
this.clearActiveIntervals();
this.teardownModalViewCountSubscription();
this.videoModalReadyPromise = null;
if (this.moderationActionController) {
try {
+31 -2
View File
@@ -47,10 +47,39 @@ import {
export const WATCH_HISTORY_VERSION_TAG_VALUE = "2";
export const isDevMode = Boolean(IS_DEV_MODE);
const runtimeScope = typeof globalThis === "undefined" ? null : globalThis;
const parseBooleanOverride = (value) => {
if (typeof value === "boolean") {
return value;
}
if (typeof value === "string") {
const normalized = value.trim().toLowerCase();
if (normalized === "true") {
return true;
}
if (normalized === "false") {
return false;
}
}
return null;
};
const devModeOverride = parseBooleanOverride(
runtimeScope?.__BITVID_DEV_MODE_OVERRIDE__
);
const verboseDevModeOverride = parseBooleanOverride(
runtimeScope?.__BITVID_VERBOSE_DEV_MODE_OVERRIDE__
);
export const isDevMode =
devModeOverride === null ? Boolean(IS_DEV_MODE) : devModeOverride;
export { IS_DEV_MODE };
export const isVerboseDevMode = Boolean(IS_VERBOSE_DEV_MODE);
export const isVerboseDevMode =
verboseDevModeOverride === null
? Boolean(IS_VERBOSE_DEV_MODE)
: verboseDevModeOverride;
export { IS_VERBOSE_DEV_MODE };
export const isLockdownMode = Boolean(IS_LOCKDOWN_MODE);
+12 -1
View File
@@ -6,6 +6,10 @@ import {
resolveInfoJsonUrl,
} from "../utils/storagePointer.js";
import { getCachedNostrTools } from "./toolkit.js";
import {
validateEventAgainstSchema,
NOTE_TYPES,
} from "../nostrEventSchemas.js";
function stringFromInput(value) {
if (value === null || value === undefined) {
@@ -934,13 +938,20 @@ export function buildNip71VideoEvent({
tags.push(...pointerTags);
}
return {
const event = {
kind: normalizeNip71Kind(metadata.kind),
pubkey: normalizedPubkey,
created_at: timestamp,
tags,
content: summary,
};
validateEventAgainstSchema(
event.kind === 22 ? NOTE_TYPES.NIP71_SHORT_VIDEO : NOTE_TYPES.NIP71_VIDEO,
event,
);
return event;
}
export function collectNip71PointerRequests(videos = []) {
+202
View File
@@ -0,0 +1,202 @@
import { devLogger } from "../utils/logger.js";
export default class VideoModalController {
constructor({
getVideoModal,
callbacks: {
showError,
getLastModalTrigger,
setLastModalTrigger,
getCurrentVideo,
} = {},
}) {
this.getVideoModal = getVideoModal;
this.showError = showError;
this.getLastModalTrigger = getLastModalTrigger;
this.setLastModalTrigger = setLastModalTrigger;
this.getCurrentVideo = getCurrentVideo;
this.videoModalReadyPromise = null;
}
get videoModal() {
return this.getVideoModal ? this.getVideoModal() : null;
}
log(message, ...args) {
devLogger.log("[VideoModalController]", message, ...args);
}
/**
* Show the modal and set the "Please stand by" poster on the video.
*/
async showModalWithPoster(video = null, options = {}) {
if (!this.videoModal) {
return null;
}
if (Object.prototype.hasOwnProperty.call(options || {}, "trigger")) {
if (this.setLastModalTrigger) {
this.setLastModalTrigger(options.trigger);
}
}
const targetVideo =
video || (this.getCurrentVideo ? this.getCurrentVideo() : null);
if (!targetVideo) {
this.log("Skipping video modal open; no target video is available.");
return null;
}
try {
const { root } = await this.ensureVideoModalReady({
ensureVideoElement: true,
});
if (!this.videoModal) {
return root || null;
}
const trigger = this.getLastModalTrigger
? this.getLastModalTrigger()
: null;
this.videoModal.open(targetVideo, {
triggerElement: trigger,
});
this.applyModalLoadingPoster();
return (
root ||
(typeof this.videoModal.getRoot === "function"
? this.videoModal.getRoot()
: null)
);
} catch (error) {
devLogger.error(
"[VideoModalController] Failed to open the video modal before playback:",
error,
);
if (this.showError) {
this.showError("Could not open the video player. Please try again.");
}
return null;
}
}
applyModalLoadingPoster() {
if (!this.videoModal) {
return;
}
this.videoModal.applyLoadingPoster();
}
forceRemoveModalPoster(reason = "manual-clear") {
if (!this.videoModal) {
return false;
}
return this.videoModal.forceRemovePoster(reason);
}
async ensureVideoModalReady({ ensureVideoElement = false } = {}) {
if (!this.videoModal) {
throw new Error("Video modal instance is not available.");
}
const getRoot = () =>
typeof this.videoModal.getRoot === "function"
? this.videoModal.getRoot()
: null;
const getVideoElement = () =>
typeof this.videoModal.getVideoElement === "function"
? this.videoModal.getVideoElement()
: null;
const existingRoot = getRoot();
const existingVideoElement = getVideoElement();
const rootConnected = Boolean(existingRoot && existingRoot.isConnected);
const hasVideoElement = Boolean(existingVideoElement);
const videoConnected = Boolean(
existingVideoElement && existingVideoElement.isConnected,
);
const needsRehydrate =
!rootConnected || !hasVideoElement || !videoConnected;
if (!needsRehydrate) {
if (
existingVideoElement &&
this.videoModal &&
typeof this.videoModal.setVideoElement === "function"
) {
const currentModalVideo =
typeof this.videoModal.getVideoElement === "function"
? this.videoModal.getVideoElement()
: null;
if (currentModalVideo !== existingVideoElement) {
this.videoModal.setVideoElement(existingVideoElement);
}
}
return {
root: existingRoot,
videoElement: existingVideoElement,
};
}
if (!videoConnected) {
if (
this.videoModal &&
typeof this.videoModal.setVideoElement === "function"
) {
this.videoModal.setVideoElement(null);
}
}
if (!this.videoModalReadyPromise) {
if (typeof this.videoModal.load !== "function") {
throw new Error("Video modal does not expose a load() method.");
}
this.videoModalReadyPromise = Promise.resolve(this.videoModal.load());
}
try {
await this.videoModalReadyPromise;
} catch (error) {
this.videoModalReadyPromise = null;
throw error;
}
this.videoModalReadyPromise = null;
const readyRoot = getRoot();
const readyVideoElement = getVideoElement();
const readyVideoConnected = Boolean(
readyVideoElement && readyVideoElement.isConnected,
);
if (readyVideoConnected) {
if (
this.videoModal &&
typeof this.videoModal.setVideoElement === "function"
) {
this.videoModal.setVideoElement(readyVideoElement);
}
} else {
if (
this.videoModal &&
typeof this.videoModal.setVideoElement === "function"
) {
this.videoModal.setVideoElement(null);
}
}
if (ensureVideoElement && !readyVideoConnected) {
throw new Error("Video modal video element is missing after load().");
}
return {
root: readyRoot,
videoElement: readyVideoConnected ? readyVideoElement : null,
};
}
}
+11
View File
@@ -1,3 +1,14 @@
[build]
command = "npm run build"
publish = "dist"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
force = true
[[headers]]
for = "/*"
[headers.values]
Content-Security-Policy = "frame-ancestors *"
+21 -96
View File
@@ -11,7 +11,11 @@
"dependencies": {
"@floating-ui/dom": "^1.7.4",
"@noble/hashes": "^2.0.1",
"@tailwindcss/typography": "^0.5.19",
"autoprefixer": "^10.4.23",
"jsdom": "^27.4.0",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.19",
"ws": "^8.19.0"
},
"devDependencies": {
@@ -19,19 +23,17 @@
"@playwright/test": "^1.58.0",
"@tailwindcss/typography": "^0.5.15",
"autoprefixer": "^10.4.23",
"cross-env": "^7.0.3",
"cross-env": "^10.1.0",
"esbuild": "^0.25.12",
"fake-indexeddb": "^6.2.5",
"nostr-tools": "^2.19.4",
"pixelmatch": "^5.3.0",
"playwright": "^1.58.0",
"pngjs": "^7.0.0",
"postcss": "^8.4.41",
"prettier": "^3.8.1",
"prettier-plugin-tailwindcss": "^0.6.5",
"serve": "^14.2.5",
"stylelint": "^16.26.1",
"tailwindcss": "^3.4.19"
"stylelint": "^16.26.1"
}
},
"node_modules/@acemir/cssom": {
@@ -44,7 +46,6 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -295,6 +296,13 @@
"url": "https://github.com/sponsors/JounQin"
}
},
"node_modules/@epic-web/invariant": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
"integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
"dev": true,
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
@@ -783,7 +791,6 @@
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
@@ -794,7 +801,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -804,14 +810,12 @@
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -895,7 +899,6 @@
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
@@ -909,7 +912,6 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
@@ -919,7 +921,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
@@ -1043,7 +1044,6 @@
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
"integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"postcss-selector-parser": "6.0.10"
@@ -1173,14 +1173,12 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
"dev": true,
"license": "MIT"
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
@@ -1215,7 +1213,6 @@
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true,
"license": "MIT"
},
"node_modules/argparse": {
@@ -1249,7 +1246,6 @@
"version": "10.4.23",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
"integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -1293,7 +1289,6 @@
"version": "2.9.18",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz",
"integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
@@ -1312,7 +1307,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -1372,7 +1366,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
@@ -1385,7 +1378,6 @@
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -1466,7 +1458,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -1476,7 +1467,6 @@
"version": "1.0.30001766",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz",
"integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -1530,7 +1520,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
@@ -1613,7 +1602,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -1734,22 +1722,21 @@
}
},
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
"integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
"dev": true,
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.1"
"@epic-web/invariant": "^1.0.0",
"cross-spawn": "^7.0.6"
},
"bin": {
"cross-env": "src/bin/cross-env.js",
"cross-env-shell": "src/bin/cross-env-shell.js"
"cross-env": "dist/bin/cross-env.js",
"cross-env-shell": "dist/bin/cross-env-shell.js"
},
"engines": {
"node": ">=10.14",
"npm": ">=6",
"yarn": ">=1"
"node": ">=20"
}
},
"node_modules/cross-spawn": {
@@ -1794,7 +1781,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true,
"license": "MIT",
"bin": {
"cssesc": "bin/cssesc"
@@ -1877,7 +1863,6 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/dir-glob": {
@@ -1897,7 +1882,6 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true,
"license": "MIT"
},
"node_modules/eastasianwidth": {
@@ -1911,7 +1895,6 @@
"version": "1.5.279",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.279.tgz",
"integrity": "sha512-0bblUU5UNdOt5G7XqGiJtpZMONma6WAfq9vsFmtn9x1+joAObr6x1chfqyxFSDCAFwFhCQDrqeAr6MYdpwJ9Hg==",
"dev": true,
"license": "ISC"
},
"node_modules/emoji-regex": {
@@ -1999,7 +1982,6 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -2057,7 +2039,6 @@
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
@@ -2084,7 +2065,6 @@
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
"dev": true,
"license": "ISC",
"dependencies": {
"reusify": "^1.0.4"
@@ -2104,7 +2084,6 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
@@ -2136,7 +2115,6 @@
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
@@ -2150,7 +2128,6 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -2165,7 +2142,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -2188,7 +2164,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
@@ -2303,7 +2278,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -2433,7 +2407,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
@@ -2446,7 +2419,6 @@
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
@@ -2478,7 +2450,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -2498,7 +2469,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
@@ -2511,7 +2481,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
@@ -2583,7 +2552,6 @@
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"license": "MIT",
"bin": {
"jiti": "bin/jiti.js"
@@ -2705,7 +2673,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
@@ -2718,7 +2685,6 @@
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.truncate": {
@@ -2778,7 +2744,6 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
@@ -2788,7 +2753,6 @@
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
@@ -2874,7 +2838,6 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0",
@@ -2886,7 +2849,6 @@
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
@@ -2915,14 +2877,12 @@
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
"dev": true,
"license": "MIT"
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -2989,7 +2949,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -2999,7 +2958,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -3096,7 +3054,6 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"license": "MIT"
},
"node_modules/path-to-regexp": {
@@ -3120,14 +3077,12 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
@@ -3140,7 +3095,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -3150,7 +3104,6 @@
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -3225,7 +3178,6 @@
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -3254,7 +3206,6 @@
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
"dev": true,
"license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.0.0",
@@ -3272,7 +3223,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
"integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -3298,7 +3248,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -3341,7 +3290,6 @@
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -3367,7 +3315,6 @@
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
@@ -3415,7 +3362,6 @@
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
@@ -3429,7 +3375,6 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
"license": "MIT"
},
"node_modules/prettier": {
@@ -3561,7 +3506,6 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true,
"funding": [
{
"type": "github",
@@ -3608,7 +3552,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"pify": "^2.3.0"
@@ -3618,7 +3561,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
@@ -3664,7 +3606,6 @@
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.1",
@@ -3695,7 +3636,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
"dev": true,
"license": "MIT",
"engines": {
"iojs": ">=1.0.0",
@@ -3706,7 +3646,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dev": true,
"funding": [
{
"type": "github",
@@ -4094,7 +4033,6 @@
"version": "3.35.1",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
"integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.2",
@@ -4147,7 +4085,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -4234,7 +4171,6 @@
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
@@ -4272,7 +4208,6 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
@@ -4285,7 +4220,6 @@
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
@@ -4299,7 +4233,6 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0"
@@ -4309,7 +4242,6 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
"dev": true,
"license": "MIT",
"dependencies": {
"thenify": ">= 3.1.0 < 4"
@@ -4322,7 +4254,6 @@
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
@@ -4339,7 +4270,6 @@
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
@@ -4357,7 +4287,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -4388,7 +4317,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
@@ -4425,7 +4353,6 @@
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/type-fest": {
@@ -4445,7 +4372,6 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -4497,7 +4423,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"license": "MIT"
},
"node_modules/vary": {
+7 -5
View File
@@ -7,7 +7,7 @@
"start": "npm run build && npx serve",
"serve": "npx serve",
"build": "node scripts/build-dist.mjs",
"build:css": "npx tailwindcss -c tailwind.config.cjs -i css/tailwind.source.css -o css/tailwind.generated.css --minify",
"build:css": "tailwindcss -c tailwind.config.cjs -i css/tailwind.source.css -o css/tailwind.generated.css --minify",
"build:beacon:bundle": "node scripts/build-beacon.mjs",
"build:beacon": "npm run build:beacon:bundle && npm run lint:inline-styles",
"ci:beacon": "npm run build:beacon",
@@ -37,25 +37,27 @@
"@playwright/test": "^1.58.0",
"@tailwindcss/typography": "^0.5.15",
"autoprefixer": "^10.4.23",
"cross-env": "^7.0.3",
"cross-env": "^10.1.0",
"esbuild": "^0.25.12",
"fake-indexeddb": "^6.2.5",
"nostr-tools": "^2.19.4",
"pixelmatch": "^5.3.0",
"playwright": "^1.58.0",
"pngjs": "^7.0.0",
"postcss": "^8.4.41",
"prettier": "^3.8.1",
"prettier-plugin-tailwindcss": "^0.6.5",
"serve": "^14.2.5",
"stylelint": "^16.26.1",
"tailwindcss": "^3.4.19"
"stylelint": "^16.26.1"
},
"license": "GPL-3.0-or-later",
"dependencies": {
"@floating-ui/dom": "^1.7.4",
"@noble/hashes": "^2.0.1",
"@tailwindcss/typography": "^0.5.19",
"autoprefixer": "^10.4.23",
"jsdom": "^27.4.0",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.19",
"ws": "^8.19.0"
}
}
+152 -182
View File
@@ -1,215 +1,185 @@
import './setup-test-env.js';
import { NostrClient } from '../../js/nostr/client.js';
import { buildVideoPostEvent, buildViewEvent, NOTE_TYPES } from '../../js/nostrEventSchemas.js';
import * as NostrTools from 'nostr-tools';
import { WebSocket } from 'ws';
if (!global.WebSocket) {
global.WebSocket = WebSocket;
}
const bytesToHex = (bytes) => {
return Buffer.from(bytes).toString('hex');
};
import { NostrClient, resolveActiveSigner } from '../../js/nostr/client.js';
import { startRelay } from './load-test-relay.mjs';
import {
buildVideoPostEvent,
buildViewEvent,
buildLegacyDirectMessageEvent
} from '../../js/nostrEventSchemas.js';
import { decryptDM } from '../../js/dmDecryptor.js';
import { generateSecretKey, getPublicKey } from 'nostr-tools';
import { bytesToHex } from '../../vendor/crypto-helpers.bundle.min.js';
const RELAYS = [
"wss://relay.damus.io",
"wss://nos.lol"
];
// Configuration
const RELAY_PORT = 8899;
const RELAY_URL = `ws://localhost:${RELAY_PORT}`;
const EPHEMERAL_SK_BYTES = generateSecretKey();
const EPHEMERAL_SK = bytesToHex(EPHEMERAL_SK_BYTES);
const EPHEMERAL_PK = getPublicKey(EPHEMERAL_SK_BYTES);
async function runTests() {
console.log("Starting Protocol & Interop Tests...");
console.log(`[Setup] Generated Ephemeral Public Key: ${EPHEMERAL_PK}`);
// --- Setup ---
console.log("[Setup] Generating ephemeral keys...");
const aliceSkBytes = NostrTools.generateSecretKey();
const aliceSk = bytesToHex(aliceSkBytes);
const alicePk = NostrTools.getPublicKey(aliceSkBytes);
async function runInteropTests() {
let relayServer;
let client;
const bobSkBytes = NostrTools.generateSecretKey();
const bobSk = bytesToHex(bobSkBytes);
const bobPk = NostrTools.getPublicKey(bobSkBytes);
try {
// 1. Start Local Relay
console.log('[Setup] Starting local test relay...');
relayServer = startRelay(RELAY_PORT);
console.log(`[Setup] Alice: ${alicePk}`);
console.log(`[Setup] Bob: ${bobPk}`);
// 2. Initialize Client
console.log('[Setup] Initializing NostrClient...');
client = new NostrClient();
client.relays = [RELAY_URL];
client.writeRelays = [RELAY_URL];
client.readRelays = [RELAY_URL];
const aliceClient = new NostrClient();
aliceClient.relays = [...RELAYS];
aliceClient.readRelays = [...RELAYS];
aliceClient.writeRelays = [...RELAYS];
// Register signer
await client.registerPrivateKeySigner({ privateKey: EPHEMERAL_SK });
console.log('[Setup] Registered private key signer.');
const bobClient = new NostrClient();
bobClient.relays = [...RELAYS];
bobClient.readRelays = [...RELAYS];
bobClient.writeRelays = [...RELAYS];
// Init client (connects to relay)
await client.init();
console.log('[Setup] Client initialized and connected.');
console.log("[Setup] Connecting clients...");
// Initialize to connect
await aliceClient.init(); // This might try to load stored session, but shouldn't matter
await bobClient.init();
// --- Test A: Video Post ---
console.log('\n[Test A] Video Post Event Roundtrip');
const videoContent = {
version: 3,
title: 'Interop Test Video',
url: 'https://example.com/video.mp4',
description: 'A test video for interoperability verification.',
mode: 'live',
videoRootId: 'interop-test-root-1'
};
// Register signers
await aliceClient.registerPrivateKeySigner({ privateKey: aliceSk, pubkey: alicePk });
aliceClient.pubkey = alicePk; // Explicitly set client pubkey to ensure correct signer resolution
const videoEventTemplate = buildVideoPostEvent({
pubkey: EPHEMERAL_PK,
created_at: Math.floor(Date.now() / 1000),
dTagValue: 'interop-test-root-1', // Using rootId as d-tag for simplicity
content: videoContent
});
await bobClient.registerPrivateKeySigner({ privateKey: bobSk, pubkey: bobPk });
bobClient.pubkey = bobPk; // Explicitly set client pubkey to ensure correct signer resolution
console.log('[Test A] Publishing Video Post...');
const { signedEvent: publishedVideo } = await client.signAndPublishEvent(videoEventTemplate, {
context: 'test-video-post'
});
let testsPassed = 0;
let testsTotal = 0;
if (!publishedVideo || !publishedVideo.id) {
throw new Error('[Test A] Failed to publish video event (no ID returned).');
}
console.log(`[Test A] Published Video Event ID: ${publishedVideo.id}`);
// --- Test A: Video Post ---
testsTotal++;
console.log("\n[Test A] Publishing VIDEO_POST...");
try {
const videoPayload = {
version: 3,
title: "Interop Test Video " + Date.now(),
description: "This is a test video event.",
videoRootId: "interop-test-" + Date.now(),
mode: "dev",
isPrivate: false,
isNsfw: false,
isForKids: false,
enableComments: true
};
// Fetch back
console.log('[Test A] Fetching event back...');
// Small delay to ensure relay processing
await new Promise(r => setTimeout(r, 200));
// We use signAndPublishEvent directly with a manually built event to test the schema helper
const event = buildVideoPostEvent({
pubkey: alicePk,
created_at: Math.floor(Date.now() / 1000),
dTagValue: videoPayload.videoRootId,
content: videoPayload
});
const fetchedVideo = await client.getEventById(publishedVideo.id, { includeRaw: true });
const { signedEvent } = await aliceClient.signAndPublishEvent(event);
console.log(`[Test A] Published event ${signedEvent.id}`);
if (!fetchedVideo) {
throw new Error('[Test A] Failed to fetch video event back from relay.');
}
// Verify
console.log("[Test A] Verifying roundtrip...");
// Give relays a moment
await new Promise(r => setTimeout(r, 2000));
const rawVideoEvent = fetchedVideo.rawEvent || fetchedVideo; // handle if getEventById returns raw or object wrapper
// getEventById returns the Video object (converted), but we can ask for raw too
const result = await bobClient.getEventById(signedEvent.id, { includeRaw: true });
if (!result || !result.rawEvent) throw new Error("Failed to fetch event back");
if (rawVideoEvent.id !== publishedVideo.id) {
throw new Error(`[Test A] ID mismatch: expected ${publishedVideo.id}, got ${rawVideoEvent.id}`);
}
const fetched = result.rawEvent;
if (fetched.id !== signedEvent.id) throw new Error("Fetched ID mismatch");
// Validate content shape (simplified check)
const parsedContent = typeof rawVideoEvent.content === 'string' ? JSON.parse(rawVideoEvent.content) : rawVideoEvent.content;
if (parsedContent.title !== videoContent.title) {
throw new Error(`[Test A] Content mismatch: expected title "${videoContent.title}", got "${parsedContent.title}"`);
}
// Parse content
const content = typeof fetched.content === 'string' ? JSON.parse(fetched.content) : fetched.content;
if (content.title !== videoPayload.title) throw new Error("Content mismatch: title");
console.log('[Test A] SUCCESS: Video Post roundtrip verified.');
console.log("[Test A] PASSED");
testsPassed++;
} catch (e) {
console.error("[Test A] FAILED:", e);
}
// --- Test B: View Event ---
testsTotal++;
console.log("\n[Test B] Publishing VIEW_EVENT...");
try {
const viewEvent = buildViewEvent({
pubkey: alicePk,
created_at: Math.floor(Date.now() / 1000),
pointerValue: "test-video-pointer", // Arbitrary pointer for test
content: "view-test"
});
// --- Test B: View Event ---
console.log('\n[Test B] View Event Visibility');
const viewEventTemplate = buildViewEvent({
pubkey: EPHEMERAL_PK,
created_at: Math.floor(Date.now() / 1000),
pointerValue: 'interop-test-root-1', // referencing the video we just created
pointerTag: ['d', 'interop-test-root-1']
});
const { signedEvent: signedView } = await aliceClient.signAndPublishEvent(viewEvent);
console.log(`[Test B] Published view event ${signedView.id}`);
console.log('[Test B] Publishing View Event...');
const { signedEvent: publishedView } = await client.signAndPublishEvent(viewEventTemplate, {
context: 'test-view-event'
});
await new Promise(r => setTimeout(r, 2000));
if (!publishedView || !publishedView.id) {
throw new Error('[Test B] Failed to publish view event.');
}
console.log(`[Test B] Published View Event ID: ${publishedView.id}`);
// Bob fetches raw event
const fetchedView = await bobClient.fetchRawEventById(signedView.id);
if (!fetchedView) throw new Error("Failed to fetch view event");
if (fetchedView.id !== signedView.id) throw new Error("View event ID mismatch");
// Fetch back to verify
await new Promise(r => setTimeout(r, 200));
const fetchedView = await client.fetchRawEventById(publishedView.id);
if (!fetchedView) {
throw new Error('[Test B] Failed to fetch view event back.');
}
console.log('[Test B] SUCCESS: View Event verified.');
console.log("[Test B] PASSED");
testsPassed++;
} catch (e) {
console.error("[Test B] FAILED:", e);
}
// --- Test C: Direct Message ---
testsTotal++;
console.log("\n[Test C] Sending Direct Message...");
try {
const message = "Hello Bob, this is Alice " + Date.now();
const npubBob = NostrTools.nip19.npubEncode(bobPk);
// --- Test C: Encrypted DM ---
console.log('\n[Test C] Encrypted DM Roundtrip (Self-DM)');
const dmMessage = "This is a secret interop message.";
const sendResult = await aliceClient.sendDirectMessage(npubBob, message);
if (!sendResult.ok) throw new Error("Failed to send DM: " + (sendResult.error || "unknown"));
console.log("[Test C] DM sent successfully");
// 1. Encrypt
const signer = resolveActiveSigner(EPHEMERAL_PK);
if (!signer || typeof signer.nip04Encrypt !== 'function') {
throw new Error('[Test C] Signer does not support NIP-04 encryption.');
}
await new Promise(r => setTimeout(r, 3000));
console.log('[Test C] Encrypting message...');
const ciphertext = await signer.nip04Encrypt(EPHEMERAL_PK, dmMessage);
console.log("[Test C] Bob checking messages...");
// Force bob to list messages (which triggers decryption using his registered signer)
const messages = await bobClient.listDirectMessages(bobPk, { limit: 5 });
// 2. Build Event
const dmTemplate = buildLegacyDirectMessageEvent({
pubkey: EPHEMERAL_PK,
created_at: Math.floor(Date.now() / 1000),
recipientPubkey: EPHEMERAL_PK,
ciphertext: ciphertext
});
// 3. Publish
console.log('[Test C] Publishing DM...');
const { signedEvent: publishedDM } = await client.signAndPublishEvent(dmTemplate, {
context: 'test-dm'
});
console.log(`[Test C] Published DM Event ID: ${publishedDM.id}`);
// 4. Fetch
await new Promise(r => setTimeout(r, 200));
const fetchedDM = await client.fetchRawEventById(publishedDM.id);
if (!fetchedDM) {
throw new Error('[Test C] Failed to fetch DM event.');
}
// 5. Decrypt using dmDecryptor.js helper
console.log('[Test C] Attempting decryption...');
// We need to construct the context expected by decryptDM
// It expects { actorPubkey, decryptors }
// We can reuse client.buildDmDecryptContext, but let's do it manually to test the decryptor isolation if possible,
// or just use the client helper to ensure we are testing the full stack availability.
// Let's use `client.decryptDirectMessageEvent` which wraps `decryptDM`.
// But the prompt asked to "attempt decryption using the appropriate decryptor helper (see js/dmDecryptor.js / docs)".
// So I'll call decryptDM directly to verify strict compliance with that instruction.
const decryptContext = await client.buildDmDecryptContext(EPHEMERAL_PK);
const decryptionResult = await decryptDM(fetchedDM, decryptContext);
if (!decryptionResult.ok) {
console.error('[Test C] Decryption errors:', decryptionResult.errors);
throw new Error('[Test C] Decryption failed.');
}
if (decryptionResult.plaintext !== dmMessage) {
throw new Error(`[Test C] Decryption mismatch: expected "${dmMessage}", got "${decryptionResult.plaintext}"`);
}
console.log(`[Test C] Decrypted text: "${decryptionResult.plaintext}"`);
console.log('[Test C] SUCCESS: Encrypted DM roundtrip verified.');
} catch (err) {
console.error('\n[Error] Test failed:', err);
process.exit(1);
} finally {
console.log('\n[Teardown] Cleaning up...');
if (client && client.pool) {
// Close pool connections to allow exit
if (typeof client.pool.close === 'function') {
client.pool.close(client.relays);
}
}
if (relayServer) {
await relayServer.close();
console.log('[Teardown] Local relay stopped.');
}
console.log('[Teardown] Done.');
process.exit(0);
const received = messages.find(m => m.plaintext === message);
if (!received) {
console.log("Received messages:", messages.map(m => m.plaintext));
throw new Error("Bob did not find the specific message");
}
if (received.sender.pubkey !== alicePk) {
console.log(`[Test C] Sender mismatch: Expected ${alicePk}, got ${received.sender.pubkey}`);
throw new Error("Sender mismatch");
}
console.log("[Test C] PASSED");
testsPassed++;
} catch (e) {
console.error("[Test C] FAILED:", e);
}
// --- Summary ---
console.log("\n---------------------------------------------------");
console.log(`Tests Completed: ${testsPassed}/${testsTotal}`);
aliceClient.logout();
bobClient.logout();
if (testsPassed === testsTotal) {
console.log("ALL TESTS PASSED");
process.exit(0);
} else {
console.error("SOME TESTS FAILED");
process.exit(1);
}
}
runInteropTests();
runTests().catch(e => {
console.error("Unhandled execution error:", e);
process.exit(1);
});
+240 -269
View File
@@ -1,286 +1,257 @@
import './setup-test-env.js';
import { indexedDB, IDBKeyRange } from "fake-indexeddb";
import { WebSocket } from 'ws';
import * as NostrTools from 'nostr-tools';
import fs from 'node:fs';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'url';
// --- Polyfills ---
if (!global.indexedDB) {
global.indexedDB = indexedDB;
}
if (!global.IDBKeyRange) {
global.IDBKeyRange = IDBKeyRange;
}
if (!global.NostrTools) {
global.NostrTools = NostrTools;
}
// setup-test-env.js handles WebSocket, crypto, localStorage, window, self, navigator
// --- Imports from Codebase ---
// Note: We need to use relative paths from scripts/agent/
import { startRelay } from './load-test-relay.mjs';
import { NostrClient } from '../../js/nostr/client.js';
import { decryptDM } from '../../js/dmDecryptor.js';
import { startRelay } from './load-test-relay.mjs';
import {
buildVideoPostEvent,
buildLegacyDirectMessageEvent
} from '../../js/nostrEventSchemas.js';
import { generateSecretKey, getPublicKey } from 'nostr-tools';
import { chromium } from 'playwright';
import { spawn } from 'child_process';
import fs from 'fs';
import path from 'path';
// --- Configuration ---
const ARTIFACTS_DIR = 'artifacts';
if (!fs.existsSync(ARTIFACTS_DIR)) {
fs.mkdirSync(ARTIFACTS_DIR, { recursive: true });
}
const RELAY_PORT = 8890;
const RELAY_PORT = 8899;
const RELAY_URL = `ws://localhost:${RELAY_PORT}`;
const HTTP_PORT = 8000;
const HTTP_URL = `http://localhost:${HTTP_PORT}`;
// --- Logging ---
const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, ''); // YYYYMMDD
const logFile = path.join(ARTIFACTS_DIR, `smoke-${dateStr}.log`);
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
// Artifacts
const ARTIFACTS_DIR = 'artifacts';
const LOG_FILE = path.join(ARTIFACTS_DIR, `smoke-${new Date().toISOString().split('T')[0].replace(/-/g, '')}.log`);
const SCREENSHOT_FILE = path.join(ARTIFACTS_DIR, 'smoke-test-ui.png');
function log(msg, ...args) {
const timestamp = new Date().toISOString();
const message = `[${timestamp}] ${msg} ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : a).join(' ')}`;
console.log(message);
logStream.write(message + '\n');
// Logger
function log(message) {
const timestamp = new Date().toISOString();
const line = `[${timestamp}] ${message}`;
console.log(line);
fs.appendFileSync(LOG_FILE, line + '\n');
}
function error(msg, ...args) {
const timestamp = new Date().toISOString();
const message = `[${timestamp}] [ERROR] ${msg} ${args.map(a => (a instanceof Error ? a.stack : (typeof a === 'object' ? JSON.stringify(a) : a))).join(' ')}`;
console.error(message);
logStream.write(message + '\n');
// Generate Keys
const bytesToHex = (bytes) => Buffer.from(bytes).toString('hex');
const EPHEMERAL_SK_BYTES = generateSecretKey();
const EPHEMERAL_SK = bytesToHex(EPHEMERAL_SK_BYTES);
const EPHEMERAL_PK = getPublicKey(EPHEMERAL_SK_BYTES);
async function startHttpServer() {
log('Starting HTTP Server...');
const serve = spawn('npx', ['serve', '.', '-p', String(HTTP_PORT)], {
stdio: 'ignore',
shell: true
});
const maxRetries = 20;
for (let i = 0; i < maxRetries; i++) {
try {
await fetch(HTTP_URL);
log('HTTP Server is ready.');
return serve;
} catch (e) {
await new Promise(r => setTimeout(r, 250));
}
}
serve.kill();
throw new Error('HTTP Server failed to start within timeout.');
}
// --- Helpers ---
async function waitForServer(url, timeoutMs = 10000) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
async function runSmokeTest() {
// Ensure artifacts dir
if (!fs.existsSync(ARTIFACTS_DIR)) {
fs.mkdirSync(ARTIFACTS_DIR);
}
fs.writeFileSync(LOG_FILE, ''); // Clear log
let relayServer;
let httpServer;
let browser;
let nodeClient;
let exitCode = 0;
try {
const res = await fetch(url);
if (res.ok) return true;
} catch (e) {
// ignore
log('--- Smoke Test Started ---');
log(`Ephemeral Pubkey: ${EPHEMERAL_PK}`);
// 1. Start Infrastructure
relayServer = startRelay(RELAY_PORT);
httpServer = await startHttpServer();
// 2. Initialize Node Client
log('Initializing Node Client...');
nodeClient = new NostrClient();
nodeClient.relays = [RELAY_URL];
nodeClient.writeRelays = [RELAY_URL];
nodeClient.readRelays = [RELAY_URL];
await nodeClient.registerPrivateKeySigner({ privateKey: EPHEMERAL_SK });
await nodeClient.init();
log('Node Client connected.');
// 3. Publish Video Event (Node)
log('Node: Publishing Video Post...');
const videoEventTemplate = buildVideoPostEvent({
pubkey: EPHEMERAL_PK,
created_at: Math.floor(Date.now() / 1000),
dTagValue: `smoke-test-${Date.now()}`,
content: {
version: 3,
title: 'Smoke Test Video',
url: 'https://example.com/video.mp4',
description: 'A video to verify feed rendering.',
mode: 'live',
videoRootId: `smoke-root-${Date.now()}`
}
});
const { signedEvent: publishedVideo } = await nodeClient.signAndPublishEvent(videoEventTemplate, {
context: 'smoke-video'
});
log(`Node: Published Video ID: ${publishedVideo.id}`);
// 4. Publish DM (Node -> Self)
log('Node: Publishing Encrypted DM...');
const dmMessage = "Smoke Test Secret Message";
const signer = await nodeClient.ensureActiveSignerForPubkey(EPHEMERAL_PK);
const ciphertext = await signer.nip04Encrypt(EPHEMERAL_PK, dmMessage);
const dmTemplate = buildLegacyDirectMessageEvent({
pubkey: EPHEMERAL_PK,
created_at: Math.floor(Date.now() / 1000),
recipientPubkey: EPHEMERAL_PK,
ciphertext: ciphertext
});
const { signedEvent: publishedDM } = await nodeClient.signAndPublishEvent(dmTemplate, {
context: 'smoke-dm'
});
log(`Node: Published DM ID: ${publishedDM.id}`);
// 5. Browser Test (Playwright)
log('Browser: Launching...');
browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
// Inject LocalStorage for Auto-Login and Relay Config
const savedProfiles = {
entries: [{ pubkey: EPHEMERAL_PK, authType: "nip07" }],
activePubkey: EPHEMERAL_PK,
version: 1
};
const relayList = [{ url: RELAY_URL, mode: "both" }];
// Note: Key format derived from investigation: bitvid:profile:<pubkey>:relayList:v1
const relayStorageKey = `bitvid:profile:${EPHEMERAL_PK}:relayList:v1`;
await context.addInitScript(({ profiles, relays, relayKey }) => {
// Disable Whitelist Mode to allow fresh pubkey login
window.localStorage.setItem('bitvid_admin_whitelist_mode', 'false');
window.localStorage.setItem('bitvid:savedProfiles:v1', JSON.stringify(profiles));
window.localStorage.setItem(relayKey, JSON.stringify(relays));
// Mock NIP-07 to avoid errors if app tries to use it (though we won't sign in browser)
window.nostr = {
getPublicKey: async () => profiles.activePubkey,
signEvent: async () => { throw new Error("Mock NIP-07 cannot sign"); }
};
}, { profiles: savedProfiles, relays: relayList, relayKey: relayStorageKey });
const page = await context.newPage();
log(`Browser: Navigating to ${HTTP_URL}...`);
// Capture Console
page.on('console', msg => log(`[Browser Console] ${msg.type()}: ${msg.text()}`));
page.on('pageerror', err => log(`[Browser Error] ${err.message}`));
await page.goto(HTTP_URL);
// Verify Login (Profile Button presence)
log('Browser: Verifying Login...');
// #profileButton might be hidden initially, wait for it
try {
await page.waitForSelector('#profileButton', { state: 'visible', timeout: 15000 });
log('Browser: Login verified (Profile button visible).');
} catch (e) {
log('Browser: Login verification failed (Profile button not found). Dumping localStorage...');
const storage = await page.evaluate(() => JSON.stringify(localStorage));
log(`[Browser Storage] ${storage}`);
await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'smoke-fail-login.png') });
}
// Verify Feed (Video Card presence)
log('Browser: Verifying Video in Feed...');
// Look for the video title
try {
// Wait longer for feed to load (relays might be slow)
await page.waitForSelector(`text=Smoke Test Video`, { timeout: 60000 });
log('Browser: Video found in feed!');
} catch (e) {
log('Browser: Video NOT found in feed. Taking screenshot...');
await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'smoke-fail-feed.png') });
// Debug: Check relays
const relayState = await page.evaluate(() => {
// Try to access global nostrClient if exposed, or check localStorage again
return {
localStorage: JSON.stringify(localStorage),
// If we can't access client directly, we rely on logs
};
});
log(`[Browser Debug] Storage: ${relayState.localStorage}`);
throw new Error('Browser: Video NOT found in feed.');
}
// Screenshot
await page.screenshot({ path: SCREENSHOT_FILE });
log(`Browser: Screenshot saved to ${SCREENSHOT_FILE}`);
// 6. Node Verification (DM Decrypt)
log('Node: Verifying DM Decryption...');
// Fetch back
await new Promise(r => setTimeout(r, 500));
const fetchedDM = await nodeClient.fetchRawEventById(publishedDM.id);
if (!fetchedDM) {
throw new Error('Node: Failed to fetch DM back.');
}
const decryptContext = await nodeClient.buildDmDecryptContext(EPHEMERAL_PK);
const decryptionResult = await decryptDM(fetchedDM, decryptContext);
if (!decryptionResult.ok) {
log(`Node: Decryption failed: ${JSON.stringify(decryptionResult.errors)}`);
throw new Error('Node: Decryption failed.');
}
if (decryptionResult.plaintext !== dmMessage) {
throw new Error(`Node: Decryption mismatch. Got "${decryptionResult.plaintext}"`);
}
log(`Node: DM Decrypted successfully: "${decryptionResult.plaintext}"`);
log('--- Smoke Test PASSED ---');
} catch (err) {
log(`--- Smoke Test FAILED: ${err.message} ---`);
if (err.stack) log(err.stack);
exitCode = 1;
} finally {
// Cleanup
log('Cleaning up...');
if (nodeClient && nodeClient.pool) {
// Close pool connections
if (typeof nodeClient.pool.close === 'function') {
nodeClient.pool.close(nodeClient.relays);
}
}
if (browser) await browser.close();
if (httpServer) httpServer.kill();
if (relayServer) await relayServer.close();
process.exit(exitCode);
}
await new Promise(r => setTimeout(r, 200));
}
return false;
}
// --- Main Test Flow ---
async function run() {
let relayServer = null;
let httpServer = null;
let client = null;
let success = true;
try {
log('--- Starting Smoke Test ---');
// 1. Start Relay
log(`Starting local relay on port ${RELAY_PORT}...`);
relayServer = startRelay(RELAY_PORT);
// Wait a bit for relay to be ready (startRelay is synchronous in setup but async in effect?)
// It uses WebSocketServer which starts immediately.
await new Promise(r => setTimeout(r, 500));
log('Relay started.');
// 2. Start HTTP Server
log(`Starting HTTP server (npx serve) on port ${HTTP_PORT}...`);
httpServer = spawn('npx', ['serve', '-p', String(HTTP_PORT)], {
stdio: 'ignore', // or 'inherit' to see output
detached: false
});
const serverReady = await waitForServer(HTTP_URL);
if (!serverReady) {
throw new Error(`HTTP server did not start on ${HTTP_URL} within timeout.`);
}
log('HTTP server is ready.');
// 3. Init NostrClient
log('Initializing NostrClient...');
client = new NostrClient();
client.relays = [RELAY_URL];
client.readRelays = [RELAY_URL];
client.writeRelays = [RELAY_URL];
// Override dev logger to avoid pollution if needed, or let it log
// We can rely on standard logging.
await client.init();
log(`NostrClient initialized and connected to ${RELAY_URL}`);
// 4. Login (Ephemeral)
log('Generating ephemeral keys for Sender...');
const senderSk = NostrTools.generateSecretKey();
const senderSkHex = Buffer.from(senderSk).toString('hex');
const senderPk = NostrTools.getPublicKey(senderSk);
log(`Sender Pubkey: ${senderPk}`);
await client.registerPrivateKeySigner({ privateKey: senderSkHex, persist: false });
client.pubkey = senderPk; // Manually set logged-in user
if (client.pubkey !== senderPk) {
throw new Error(`Client pubkey mismatch. Expected ${senderPk}, got ${client.pubkey}`);
}
log('Sender logged in.');
// 5. Publish Video
log('Publishing test video...');
const videoPayload = {
title: `Smoke Test Video ${Date.now()}`,
description: 'Smoke test description',
magnet: `magnet:?xt=urn:btih:${Math.random().toString(16).slice(2)}`,
url: `${HTTP_URL}/test.mp4`, // Fake URL
thumbnail: `${HTTP_URL}/thumb.jpg`,
mode: 'live',
videoRootId: `smoke-${Date.now()}`,
isNsfw: false,
isForKids: true
};
const publishedEvent = await client.publishVideo(videoPayload, senderPk);
if (!publishedEvent || !publishedEvent.id) {
throw new Error('Failed to publish video event.');
}
log(`Video published. Event ID: ${publishedEvent.id}`);
// Verify Read Back
log('Verifying video can be fetched...');
// We use subscribeVideos for main feed, or getEventById.
// Let's use getEventById for direct verification.
const fetched = await client.getEventById(publishedEvent.id);
if (!fetched) {
throw new Error('Failed to fetch published video.');
}
if (fetched.title !== videoPayload.title) {
throw new Error(`Fetched video title mismatch. Expected "${videoPayload.title}", got "${fetched.title}"`);
}
log('Video fetched successfully.');
// 6. DM Flow
log('Testing DM Flow...');
const recipientSk = NostrTools.generateSecretKey();
const recipientSkHex = Buffer.from(recipientSk).toString('hex');
const recipientPk = NostrTools.getPublicKey(recipientSk);
const recipientNpub = NostrTools.nip19.npubEncode(recipientPk);
log(`Recipient Pubkey: ${recipientPk}`);
const dmMessage = `Smoke Test Message ${Date.now()}`;
// Send DM
const sendResult = await client.sendDirectMessage(recipientNpub, dmMessage);
if (!sendResult.ok) {
throw new Error(`Failed to send DM: ${sendResult.error}`);
}
log('DM sent successfully.');
// Verify Decryption (Simulate Recipient)
log('Verifying DM decryption (Recipient view)...');
// We need to find the DM event.
// Since we don't have a second client syncing, we can manually query the pool/relay.
const filter = {
kinds: [4, 1059], // 4 for legacy, 1059 for NIP-17
'#p': [recipientPk],
limit: 1
};
// wait a moment for propagation
await new Promise(r => setTimeout(r, 200));
const events = await client.pool.list([RELAY_URL], [filter]);
if (events.length === 0) {
throw new Error('Recipient could not find DM event on relay.');
}
const dmEvent = events[0];
log(`Found DM event kind ${dmEvent.kind} id ${dmEvent.id}`);
// Construct decryption context for decryptDM
// We need a decryptor for the recipient.
// Since we have the recipient's private key, we can use NostrTools.nip04 or nip44.
const decryptors = [];
// NIP-04 Decryptor
decryptors.push({
scheme: 'nip04',
decrypt: async (remotePubkey, ciphertext) => {
return await NostrTools.nip04.decrypt(recipientSkHex, remotePubkey, ciphertext);
}
});
// NIP-44 Decryptor
decryptors.push({
scheme: 'nip44',
supportsGiftWrap: true,
decrypt: (remotePubkey, ciphertext) => {
const conversationKey = NostrTools.nip44.v2.utils.getConversationKey(recipientSk, remotePubkey);
return NostrTools.nip44.v2.decrypt(ciphertext, conversationKey);
}
});
const context = {
actorPubkey: recipientPk,
decryptors
};
const decryptResult = await decryptDM(dmEvent, context);
if (!decryptResult.ok) {
throw new Error(`DM decryption failed: ${JSON.stringify(decryptResult.errors)}`);
}
if (decryptResult.plaintext !== dmMessage) {
throw new Error(`DM plaintext mismatch. Expected "${dmMessage}", got "${decryptResult.plaintext}"`);
}
log('DM verified successfully.');
} catch (err) {
success = false;
error('Smoke Test Failed!', err);
} finally {
// Report
const summary = {
timestamp: new Date().toISOString(),
success,
logFile
};
const summaryFile = path.join(ARTIFACTS_DIR, `smoke-summary-${dateStr}.json`);
fs.writeFileSync(summaryFile, JSON.stringify(summary, null, 2));
log(`Summary saved to ${summaryFile}`);
// Teardown
log('Teardown...');
if (client) {
// client.logout(); // Cleans up
// Close pool connections?
// client.pool.close(client.relays); // SimplePool doesn't have explicit close usually?
// NostrTools SimplePool usually manages connections.
// We can just rely on process exit.
}
if (relayServer) {
relayServer.close();
log('Relay stopped.');
}
if (httpServer) {
httpServer.kill();
log('HTTP server stopped.');
}
log('Done.');
process.exit(success ? 0 : 1);
}
}
run();
runSmokeTest();
+9 -2
View File
@@ -147,19 +147,26 @@ function collectGenericLogs(filepath, sourceName) {
for (const line of lines) {
// Simple heuristic for generic logs: look for "Error:" or "Exception:"
// and capture following lines that look like stack traces (start with 'at ' or are indented)
// Also support bracketed log levels like [ERROR], [FAIL], [CRITICAL], [WARN]
const isErrorStart = /Error:|Exception:|FAIL|CRITICAL/.test(line);
const errorMatch = line.match(/(?:\[\s*(?:ERROR|FAIL|CRITICAL|EXCEPTION|WARN|WARNING)\s*\])|(?:\b(?:Error|Exception|Warning):)|(?:\b(?:FAIL|CRITICAL)\b)/i);
const isErrorStart = !!errorMatch;
const isStackLine = /^\s+at /.test(line) || /^\s+/.test(line); // Indented lines often follow errors
if (isErrorStart) {
if (currentError) {
errors.push(currentError);
}
let severity = 'High';
if (line.match(/CRITICAL/i)) severity = 'Critical';
else if (line.match(/WARN|WARNING/i)) severity = 'Medium';
currentError = {
source: sourceName,
title: sanitize(line.trim()),
details: [sanitize(line.trim())],
severity: 'High'
severity: severity
};
} else if (currentError && isStackLine) {
currentError.details.push(sanitize(line.trim()));
+246 -247
View File
@@ -1,9 +1,48 @@
#!/usr/bin/env node
// scripts/agent/validate-events.mjs
import WebSocket from 'ws';
import { webcrypto } from 'node:crypto';
// Polyfill WebSocket
if (typeof globalThis.WebSocket === 'undefined') {
globalThis.WebSocket = WebSocket;
}
// Polyfill window and self
if (typeof globalThis.window === 'undefined') {
globalThis.window = globalThis;
}
if (typeof globalThis.self === 'undefined') {
globalThis.self = globalThis;
}
// Polyfill crypto
if (typeof globalThis.crypto === 'undefined') {
globalThis.crypto = webcrypto;
}
// Polyfill localStorage
if (typeof globalThis.localStorage === 'undefined') {
const storage = new Map();
globalThis.localStorage = {
getItem: (k) => storage.get(k) || null,
setItem: (k, v) => storage.set(String(k), String(v)),
removeItem: (k) => storage.delete(k),
clear: () => storage.clear(),
key: (i) => Array.from(storage.keys())[i] || null,
get length() { return storage.size; }
};
}
if (typeof globalThis.window.localStorage === 'undefined') {
globalThis.window.localStorage = globalThis.localStorage;
}
// Mock navigator
if (typeof globalThis.navigator === 'undefined') {
globalThis.navigator = { userAgent: 'node' };
}
import { execSync } from "child_process";
import {
NOTE_TYPES,
validateEventStructure,
buildVideoPostEvent,
buildHttpAuthEvent,
buildReportEvent,
@@ -28,352 +67,312 @@ import {
buildBlockListEvent,
buildHashtagPreferenceEvent,
buildAdminListEvent,
} from "../../js/nostrEventSchemas.js";
validateEventStructure,
NOTE_TYPES,
ADMIN_LIST_IDENTIFIERS
} from '../../js/nostrEventSchemas.js';
import { buildNip71VideoEvent } from '../../js/nostr/nip71.js';
const PUBKEY = "0000000000000000000000000000000000000000000000000000000000000001";
const CREATED_AT = 1700000000;
// Note: Most builders are defined in js/nostrEventSchemas.js and are already instrumented
// with validation checks (validateEventAgainstSchema) when running in Dev Mode.
// buildNip71VideoEvent was instrumented in js/nostr/nip71.js as part of this work.
// This script verifies that all builders produce valid events according to the schema.
const IGNORED_BUILDERS = new Set([
"buildShareUrlFromEventId", // Method in js/app.js, not an event builder
"buildShareUrlFromEvent", // Substring match or alias
"buildListEvent", // Local helper in js/adminListStore.js wrapping buildAdminListEvent
"buildProfileFromEvent", // Local helper in js/services/authService.js
]);
const KNOWN_BUILDERS = {
buildVideoPostEvent: {
builder: buildVideoPostEvent,
const testCases = [
{
name: 'Video Post',
type: NOTE_TYPES.VIDEO_POST,
builder: buildVideoPostEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
dTagValue: "test-video-id",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
dTagValue: 'test-video',
content: {
version: 3,
title: "Test Video",
videoRootId: "test-video-id",
infoHash: "0123456789abcdef0123456789abcdef01234567",
},
title: 'Test Video',
videoRootId: 'test-root',
}
}
},
buildHttpAuthEvent: {
builder: buildHttpAuthEvent,
{
name: 'HTTP Auth',
type: NOTE_TYPES.HTTP_AUTH,
builder: buildHttpAuthEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
url: "https://example.com/auth",
method: "GET",
payload: "hash",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
url: 'https://example.com',
method: 'GET'
}
},
buildReportEvent: {
builder: buildReportEvent,
{
name: 'Report',
type: NOTE_TYPES.REPORT,
builder: buildReportEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
eventId: "e".repeat(64),
reportType: "nudity",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
eventId: '0000000000000000000000000000000000000000000000000000000000000002',
reportType: 'nudity'
}
},
buildVideoMirrorEvent: {
builder: buildVideoMirrorEvent,
{
name: 'Video Mirror',
type: NOTE_TYPES.VIDEO_MIRROR,
builder: buildVideoMirrorEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
tags: [["url", "https://example.com/video.mp4"], ["m", "video/mp4"]],
content: "Mirroring video",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
content: 'Alt text'
}
},
buildRepostEvent: {
builder: buildRepostEvent,
{
name: 'Repost',
type: NOTE_TYPES.REPOST,
builder: buildRepostEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
eventId: "e".repeat(64),
eventRelay: "wss://relay.example.com",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
eventId: '0000000000000000000000000000000000000000000000000000000000000002',
targetKind: 1
}
},
buildShareEvent: {
builder: buildShareEvent,
{
name: 'Share',
type: NOTE_TYPES.SHARE,
builder: buildShareEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
video: { id: "e".repeat(64), pubkey: PUBKEY },
relays: ["wss://relay.example.com"],
content: "Check this out",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
content: 'Check this out',
video: { id: '0000000000000000000000000000000000000000000000000000000000000002' }
}
},
buildRelayListEvent: {
builder: buildRelayListEvent,
{
name: 'Relay List',
type: NOTE_TYPES.RELAY_LIST,
builder: buildRelayListEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
relays: [{ url: "wss://relay.example.com", read: true, write: true }],
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
relays: ['wss://relay.example.com']
}
},
buildDmRelayListEvent: {
builder: buildDmRelayListEvent,
{
name: 'DM Relay List',
type: NOTE_TYPES.DM_RELAY_LIST,
builder: buildDmRelayListEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
relays: ["wss://relay.example.com"],
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
relays: ['wss://relay.example.com']
}
},
buildProfileMetadataEvent: {
builder: buildProfileMetadataEvent,
{
name: 'Profile Metadata',
type: NOTE_TYPES.PROFILE_METADATA,
builder: buildProfileMetadataEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
metadata: { name: "Test User", about: "Testing" },
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
metadata: { name: 'Alice' }
}
},
buildMuteListEvent: {
builder: buildMuteListEvent,
{
name: 'Mute List',
type: NOTE_TYPES.MUTE_LIST,
builder: buildMuteListEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
pTags: [PUBKEY],
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
pTags: ['0000000000000000000000000000000000000000000000000000000000000002']
}
},
buildDeletionEvent: {
builder: buildDeletionEvent,
{
name: 'Deletion',
type: NOTE_TYPES.DELETION,
builder: buildDeletionEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
eventIds: ["e".repeat(64)],
reason: "mistake",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
eventIds: ['0000000000000000000000000000000000000000000000000000000000000002']
}
},
buildLegacyDirectMessageEvent: {
builder: buildLegacyDirectMessageEvent,
{
name: 'Legacy DM',
type: NOTE_TYPES.LEGACY_DM,
builder: buildLegacyDirectMessageEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
recipientPubkey: PUBKEY,
ciphertext: "encrypted",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
recipientPubkey: '0000000000000000000000000000000000000000000000000000000000000002',
ciphertext: 'base64ciphertext'
}
},
buildDmAttachmentEvent: {
builder: buildDmAttachmentEvent,
{
name: 'DM Attachment',
type: NOTE_TYPES.DM_ATTACHMENT,
builder: buildDmAttachmentEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
recipientPubkey: PUBKEY,
attachment: { url: "https://example.com/file", x: "hash", type: "image/jpeg" },
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
recipientPubkey: '0000000000000000000000000000000000000000000000000000000000000002',
attachment: { url: 'https://example.com/file.jpg', x: 'hash' }
}
},
buildDmReadReceiptEvent: {
builder: buildDmReadReceiptEvent,
{
name: 'DM Read Receipt',
type: NOTE_TYPES.DM_READ_RECEIPT,
builder: buildDmReadReceiptEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
recipientPubkey: PUBKEY,
eventId: "e".repeat(64),
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
recipientPubkey: '0000000000000000000000000000000000000000000000000000000000000002',
eventId: '0000000000000000000000000000000000000000000000000000000000000002'
}
},
buildDmTypingIndicatorEvent: {
builder: buildDmTypingIndicatorEvent,
{
name: 'DM Typing Indicator',
type: NOTE_TYPES.DM_TYPING,
builder: buildDmTypingIndicatorEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
recipientPubkey: PUBKEY,
expiresAt: CREATED_AT + 60,
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
recipientPubkey: '0000000000000000000000000000000000000000000000000000000000000002',
expiresAt: 1234567900
}
},
buildViewEvent: {
builder: buildViewEvent,
{
name: 'View Event',
type: NOTE_TYPES.VIEW_EVENT,
builder: buildViewEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
pointerValue: "e".repeat(64),
pointerTag: ["e", "e".repeat(64)],
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
pointerValue: '0000000000000000000000000000000000000000000000000000000000000002'
}
},
buildZapRequestEvent: {
builder: buildZapRequestEvent,
{
name: 'Zap Request',
type: NOTE_TYPES.ZAP_REQUEST,
builder: buildZapRequestEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
recipientPubkey: PUBKEY,
amountSats: 100,
relays: ["wss://relay.example.com"],
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
recipientPubkey: '0000000000000000000000000000000000000000000000000000000000000002',
amountSats: 100
}
},
buildReactionEvent: {
builder: buildReactionEvent,
{
name: 'Reaction',
type: NOTE_TYPES.VIDEO_REACTION,
builder: buildReactionEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
pointerValue: "e".repeat(64),
targetPointer: { type: "e", value: "e".repeat(64) },
content: "+",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
pointerValue: '0000000000000000000000000000000000000000000000000000000000000002',
content: '+'
}
},
buildCommentEvent: {
builder: buildCommentEvent,
{
name: 'Comment',
type: NOTE_TYPES.VIDEO_COMMENT,
builder: buildCommentEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
videoEventId: "e".repeat(64),
content: "Nice video",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
videoEventId: '0000000000000000000000000000000000000000000000000000000000000002',
content: 'Nice video'
}
},
buildWatchHistoryEvent: {
builder: buildWatchHistoryEvent,
{
name: 'Watch History',
type: NOTE_TYPES.WATCH_HISTORY,
builder: buildWatchHistoryEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
monthIdentifier: "2023-01",
content: { version: 2, month: "2023-01", items: [] },
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
monthIdentifier: 'watch-history:2025-01',
content: { version: 2, month: '2025-01', items: [] }
}
},
buildSubscriptionListEvent: {
builder: buildSubscriptionListEvent,
{
name: 'Subscription List',
type: NOTE_TYPES.SUBSCRIPTION_LIST,
builder: buildSubscriptionListEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
content: "encrypted-json",
encryption: "nip44_v2",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
content: 'encrypted-payload'
}
},
buildBlockListEvent: {
builder: buildBlockListEvent,
{
name: 'Block List',
type: NOTE_TYPES.USER_BLOCK_LIST,
builder: buildBlockListEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
content: "encrypted-json",
encryption: "nip44_v2",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
content: 'encrypted-payload'
}
},
buildHashtagPreferenceEvent: {
builder: buildHashtagPreferenceEvent,
{
name: 'Hashtag Preferences',
type: NOTE_TYPES.HASHTAG_PREFERENCES,
builder: buildHashtagPreferenceEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
content: "encrypted-json",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
content: JSON.stringify({ version: 1, interests: [], disinterests: [] })
}
},
buildAdminListEvent: {
builder: buildAdminListEvent,
{
name: 'Admin List (Moderation)',
type: NOTE_TYPES.ADMIN_MODERATION_LIST,
// Special handling for args
args: ["moderation", {
pubkey: PUBKEY,
created_at: CREATED_AT,
hexPubkeys: [PUBKEY],
}]
builder: (params) => buildAdminListEvent(ADMIN_LIST_IDENTIFIERS.moderation, params),
params: {
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
hexPubkeys: []
}
},
};
function getUsedBuilders() {
console.log("Scanning repo for event builder usage...");
try {
const output = execSync('grep -r "build[A-Za-z]\\+Event" js/', { encoding: 'utf-8' });
const lines = output.split('\n');
const usedBuilders = new Set();
const regex = /(build[A-Za-z]+Event)/g;
lines.forEach(line => {
// Skip the schemas file itself
if (line.includes('js/nostrEventSchemas.js')) return;
const matches = line.match(regex);
if (matches) {
matches.forEach(match => usedBuilders.add(match));
{
name: 'NIP-71 Video',
type: NOTE_TYPES.NIP71_VIDEO,
builder: buildNip71VideoEvent,
params: {
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
title: 'Test NIP-71',
metadata: { kind: 21, title: 'Test NIP-71' },
pointerIdentifiers: { videoRootId: 'test-root' }
}
});
return usedBuilders;
} catch (error) {
// Grep returns status 1 if no matches found, which might throw an error in execSync
// depending on the version/platform, or just empty output.
console.error("Grep failed or found nothing:", error.message);
return new Set();
}
}
];
async function run() {
const usedBuilders = getUsedBuilders();
console.log(`Found ${usedBuilders.size} unique builder(s) used in the codebase.`);
// also check if we missed any exported builder in our KNOWN_BUILDERS list
// by assuming KNOWN_BUILDERS contains all we want to support.
let failureCount = 0;
const missingTests = [];
usedBuilders.forEach(builderName => {
if (!KNOWN_BUILDERS[builderName] && !IGNORED_BUILDERS.has(builderName)) {
missingTests.push(builderName);
}
});
if (missingTests.length > 0) {
console.warn("\n[WARNING] The following builders are used in the repo but have no test case in this script:");
missingTests.forEach(name => console.warn(` - ${name}`));
// We don't fail the build for this yet, but we warn.
// Or maybe we should fail? The prompt said "Check runtime event construction...".
// Let's count it as a "gap" but allow passing if validation of known ones works.
}
console.log("\nValidating builders...");
for (const [name, config] of Object.entries(KNOWN_BUILDERS)) {
// If we want to be strict, we could check if it is used. But validation is good regardless.
const isUsed = usedBuilders.has(name);
const usageLabel = isUsed ? "(USED)" : "(UNUSED)";
let failed = false;
for (const testCase of testCases) {
try {
let event;
if (config.args) {
event = config.builder(...config.args);
} else {
event = config.builder(config.params);
}
const { valid, errors } = validateEventStructure(config.type, event);
if (valid) {
console.log(`[PASS] ${name} ${usageLabel}`);
} else {
console.error(`[FAIL] ${name} ${usageLabel}`);
errors.forEach((err) => console.error(` - ${err}`));
failureCount++;
}
} catch (e) {
console.error(`[ERROR] ${name} ${usageLabel} threw an exception:`, e);
failureCount++;
const event = testCase.builder(testCase.params);
if (!event) {
console.error(`[FAIL] ${testCase.name}: Builder returned null`);
failed = true;
continue;
}
const { valid, errors } = validateEventStructure(testCase.type, event);
if (!valid) {
console.error(`[FAIL] ${testCase.name}:`);
errors.forEach(err => console.error(` - ${err}`));
failed = true;
} else {
console.log(`[PASS] ${testCase.name}`);
}
} catch (err) {
console.error(`[FAIL] ${testCase.name} threw error:`, err);
failed = true;
}
}
if (failureCount > 0) {
console.error(`\nValidation failed with ${failureCount} errors.`);
process.exit(1);
} else {
console.log("\nAll known builders validated successfully.");
process.exit(0);
}
}
run();
if (failed) {
process.exit(1);
}
+92 -24
View File
@@ -26,9 +26,8 @@ function runCommand(command, args = [], options = {}) {
return result;
}
function getChangedFiles() {
function getChangedFiles(baseRef) {
try {
const baseRef = process.env.GITHUB_BASE_REF || 'main';
const baseUrl = process.env.GITHUB_BASE_REPO_URL;
let remote = 'origin';
@@ -64,7 +63,6 @@ function getChangedFiles() {
console.error('Error getting changed files:', error.stderr || error.message);
// Fallback: try 2-dot diff if 3-dot failed (e.g. shallow clone issues)
try {
const baseRef = process.env.GITHUB_BASE_REF || 'main';
const remote = process.env.GITHUB_BASE_REPO_URL ? 'upstream' : 'origin';
const cmd = `git diff --name-only ${remote}/${baseRef} HEAD`;
console.log(`Retrying with 2-dot diff: ${cmd}`);
@@ -77,8 +75,7 @@ function getChangedFiles() {
}
}
function checkReleaseChannel(changedFiles) {
const baseRef = process.env.GITHUB_BASE_REF || 'main';
function checkReleaseChannel(changedFiles, baseRef) {
const warnings = [];
if (baseRef === 'main') {
@@ -100,13 +97,11 @@ function checkReleaseChannel(changedFiles) {
return warnings;
}
async function main() {
try {
console.log('Starting Automated PR Review...');
let commentBody = '## 🤖 Automated PR Review\n\n';
let hasFailures = false;
async function reviewPR(prNumber, baseRef) {
console.log(`Starting Automated PR Review for PR #${prNumber} (base: ${baseRef})...`);
let commentBody = '## 🤖 Automated PR Review\n\n';
let hasFailures = false;
let hasFormatChanges = false;
let microFixesStatus = '';
// 1. Install Dependencies
if (process.env.CI || process.argv.includes('--force-ci')) {
@@ -149,13 +144,13 @@ async function main() {
}
// 5. Security/Protocol Review Flags
const changedFiles = getChangedFiles();
const changedFiles = getChangedFiles(baseRef);
const sensitiveChanges = changedFiles.filter(file =>
SENSITIVE_PATHS.some(pattern => pattern.test(file))
);
// Release Channel Check
const channelWarnings = checkReleaseChannel(changedFiles);
const channelWarnings = checkReleaseChannel(changedFiles, baseRef);
if (channelWarnings.length > 0) {
commentBody += '### 🚀 Release Channel Checks\n\n';
channelWarnings.forEach(w => commentBody += `${w}\n\n`);
@@ -182,7 +177,7 @@ async function main() {
commentBody += '\n';
// 6. Post Comment (if in CI)
if (process.env.GITHUB_TOKEN && process.env.PR_NUMBER) {
if (process.env.GITHUB_TOKEN && prNumber) {
// Verify gh CLI is available
console.log('Verifying GitHub CLI...');
runCommand('gh', ['--version']);
@@ -194,7 +189,7 @@ async function main() {
execSync('git config user.name "bitvid-agent"');
execSync('git config user.email "[email protected]"');
execSync('git add .');
execSync('git commit -m "fix(ai): formatting (agent)"');
execSync('git commit -m "fix(ai): formatting (agent) suggested"');
execSync('git push');
commentBody += '> **✅ Micro-fixes applied:** Formatting fixes have been committed to this branch.\n\n';
@@ -204,13 +199,11 @@ async function main() {
// Fallback: Create a new branch and open a PR
try {
console.log('Attempting to create a follow-up PR with fixes...');
const prNumber = process.env.PR_NUMBER;
const fixBranchName = `agent-fix/pr-${prNumber}-${Date.now()}`;
// Push the current HEAD (with fixes) to a new branch on origin
execSync(`git push origin HEAD:refs/heads/${fixBranchName}`);
const baseRef = process.env.GITHUB_BASE_REF || 'main';
const prTitle = `fix(ai): formatting fixes for PR #${prNumber}`;
const prBody = `This PR applies automated formatting fixes for PR #${prNumber}.`;
@@ -231,9 +224,9 @@ async function main() {
}
}
console.log('Posting comment to PR #' + process.env.PR_NUMBER);
console.log('Posting comment to PR #' + prNumber);
try {
const gh = spawnSync('gh', ['pr', 'comment', process.env.PR_NUMBER, '-F', '-'], {
const gh = spawnSync('gh', ['pr', 'comment', prNumber, '-F', '-'], {
input: commentBody,
stdio: ['pipe', 'inherit', 'inherit']
});
@@ -247,17 +240,92 @@ async function main() {
}
// Always print the report to stdout so it's visible in logs (especially if posting fails or for debugging)
// Always print the report to stdout
console.log('---------------------------------------------------');
console.log('PR Review Report:');
console.log(`PR Review Report for PR #${prNumber}:`);
console.log(commentBody);
console.log('---------------------------------------------------');
if (hasFailures) {
process.exit(1);
if (hasFailures) {
throw new Error('Lint or Test failures detected.');
}
}
async function main() {
try {
const isAll = process.argv.includes('--all');
if (isAll) {
console.log('Fetching all open PRs...');
// Get list of open PRs
const listCmd = ['pr', 'list', '--state', 'open', '--json', 'number,baseRefName,headRefName'];
const result = spawnSync('gh', listCmd, { encoding: 'utf-8' });
if (result.error || result.status !== 0) {
throw new Error(`Failed to list PRs: ${result.stderr || result.error?.message}`);
}
const prs = JSON.parse(result.stdout);
console.log(`Found ${prs.length} open PRs.`);
let hasGlobalFailures = false;
// Save current branch to restore later
let originalBranch;
try {
originalBranch = execSync('git branch --show-current', { encoding: 'utf-8' }).trim();
} catch (e) {
// Ignore
}
for (const pr of prs) {
try {
console.log(`\n=== Processing PR #${pr.number} ===`);
// Checkout PR
runCommand('gh', ['pr', 'checkout', pr.number]);
// Run Review
await reviewPR(pr.number, pr.baseRefName);
} catch (e) {
console.error(`❌ Failed to process PR #${pr.number}:`, e.message);
hasGlobalFailures = true;
// Continue to next PR
} finally {
// Clean up or reset if needed?
// Since we use gh pr checkout, we are on a different branch.
// Next iteration will checkout another branch.
}
}
if (originalBranch) {
console.log(`Restoring original branch: ${originalBranch}`);
try {
execSync(`git checkout ${originalBranch}`);
} catch(e) {
console.error('Failed to restore original branch');
}
}
if (hasGlobalFailures) {
process.exit(1);
}
} else {
const prNumber = process.env.PR_NUMBER;
const baseRef = process.env.GITHUB_BASE_REF || 'main';
if (prNumber) {
await reviewPR(prNumber, baseRef);
} else {
// Local run or manual invocation without specific PR env vars
console.log('No PR_NUMBER found and no --all flag. Running local review against ' + baseRef);
await reviewPR('local', baseRef);
}
}
} catch (error) {
console.error('Automated PR Review failed:', error);
console.error('Automated PR Review failed:', error.message);
process.exit(1);
}
}
+3
View File
@@ -4,6 +4,9 @@ import fs from "node:fs";
import { fileURLToPath } from "node:url";
import path from "node:path";
// Ensure NODE_ENV is set to "test" so that nostrToolsBootstrap.js uses the mock toolkit
process.env.NODE_ENV = "test";
const rootDir = path.dirname(fileURLToPath(new URL("../package.json", import.meta.url)));
const testsDir = path.join(rootDir, "tests");
const testFiles = [];
+5
View File
@@ -55,6 +55,9 @@ test("batchFetchProfiles handles fast and failing relays", async () => {
const originalWriteRelays = Array.isArray(nostrClient.writeRelays)
? [...nostrClient.writeRelays]
: nostrClient.writeRelays;
const originalReadRelays = Array.isArray(nostrClient.readRelays)
? [...nostrClient.readRelays]
: nostrClient.readRelays;
const originalPool = nostrClient.pool;
const poolListCalls = [];
@@ -62,6 +65,7 @@ test("batchFetchProfiles handles fast and failing relays", async () => {
nostrClient.relays = [FAST_RELAY, FAIL_RELAY];
nostrClient.readRelays = [FAST_RELAY, FAIL_RELAY];
nostrClient.writeRelays = [FAST_RELAY, FAIL_RELAY];
nostrClient.readRelays = [FAST_RELAY, FAIL_RELAY];
nostrClient.pool = {
list: (relays, filters) => {
poolListCalls.push({ relays, filters });
@@ -98,6 +102,7 @@ test("batchFetchProfiles handles fast and failing relays", async () => {
nostrClient.relays = originalRelays;
nostrClient.readRelays = originalReadRelays;
nostrClient.writeRelays = originalWriteRelays;
nostrClient.readRelays = originalReadRelays;
nostrClient.pool = originalPool;
}
+2
View File
@@ -273,7 +273,9 @@ function createController(options = {}) {
}
return hashtagStore.disinterests.delete(normalized);
},
load: async () => {},
publish: async () => ({ ok: true }),
load: async () => {},
on: () => () => {},
load: async () => {},
};
+113
View File
@@ -0,0 +1,113 @@
import test from "node:test";
import assert from "node:assert/strict";
import VideoModalController from "../js/ui/videoModalController.js";
test("VideoModalController", async (t) => {
await t.test("ensureVideoModalReady throws if modal is missing", async () => {
const controller = new VideoModalController({
getVideoModal: () => null,
});
await assert.rejects(
controller.ensureVideoModalReady(),
/Video modal instance is not available/
);
});
await t.test("ensureVideoModalReady loads modal if needs rehydrate", async () => {
let loadCalled = false;
let setVideoElementCalled = false;
const mockRoot = { isConnected: true };
const mockVideoElement = { isConnected: true };
const mockModal = {
// First call returns null (simulate missing root), subsequent calls return valid
getRoot: (() => {
let calls = 0;
return () => {
calls++;
return calls === 1 ? null : mockRoot;
};
})(),
getVideoElement: () => mockVideoElement,
load: async () => {
loadCalled = true;
},
setVideoElement: (el) => {
setVideoElementCalled = true;
assert.equal(el, mockVideoElement);
},
};
const controller = new VideoModalController({
getVideoModal: () => mockModal,
});
const result = await controller.ensureVideoModalReady();
assert.equal(loadCalled, true, "Should call load()");
assert.equal(setVideoElementCalled, true, "Should set video element");
assert.equal(result.root, mockRoot);
assert.equal(result.videoElement, mockVideoElement);
});
await t.test("showModalWithPoster uses provided video", async () => {
const mockVideo = { id: "v1" };
let openedVideo = null;
let appliedPoster = false;
const mockModal = {
getRoot: () => ({ isConnected: true }),
getVideoElement: () => ({ isConnected: true }),
load: async () => {},
open: (video) => { openedVideo = video; },
applyLoadingPoster: () => { appliedPoster = true; },
setVideoElement: () => {},
};
const controller = new VideoModalController({
getVideoModal: () => mockModal,
});
await controller.showModalWithPoster(mockVideo);
assert.deepEqual(openedVideo, mockVideo);
assert.equal(appliedPoster, true);
});
await t.test("showModalWithPoster falls back to current video", async () => {
const currentVideo = { id: "current" };
let openedVideo = null;
const mockModal = {
getRoot: () => ({ isConnected: true }),
getVideoElement: () => ({ isConnected: true }),
load: async () => {},
open: (video) => { openedVideo = video; },
applyLoadingPoster: () => {},
setVideoElement: () => {},
};
const controller = new VideoModalController({
getVideoModal: () => mockModal,
callbacks: {
getCurrentVideo: () => currentVideo,
}
});
await controller.showModalWithPoster(); // No video passed
assert.deepEqual(openedVideo, currentVideo);
});
await t.test("forceRemoveModalPoster calls modal method", async () => {
let removeReason = null;
const mockModal = {
forceRemovePoster: (reason) => { removeReason = reason; return true; }
};
const controller = new VideoModalController({
getVideoModal: () => mockModal,
});
controller.forceRemoveModalPoster("test-reason");
assert.equal(removeReason, "test-reason");
});
});