mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-31 09:52:24 +00:00
61 files changed
+2528
-3267
No files matched your search
+3
-2
@@ -40,12 +40,13 @@ Automated agents contributing to this repository should follow these rules:
|
||||
- **Commit Messages**: Use the convention `type(scope): description (agent)` (e.g., `fix(ai): formatting (agent)` or `docs(ai): update quickstart (agent)`).
|
||||
- **Reference Issues**: Link to relevant issues in the PR description.
|
||||
- **Review AGENTS.md**: Always consult `AGENTS.md` for specific architectural guidelines and constraints before starting work.
|
||||
- **Efficiency**: Prefer running targeted or sharded tests (e.g., `npm run test:unit:shard1`) to conserve resources during iteration.
|
||||
|
||||
## Submitting a Pull Request
|
||||
|
||||
1. Fork the repository and create a new branch from `main`.
|
||||
2. Make your changes, adding tests if applicable.
|
||||
3. Run `npm run format`, `npm run test:unit`, and `npm run lint` to ensure quality.
|
||||
3. Run `npm run format`, `npm run lint`, and unit tests (e.g. `npm run test:unit` or a shard like `npm run test:unit:shard1`) to ensure quality.
|
||||
4. (Optional) Run `npm run test:visual` if you made UI changes.
|
||||
5. (Optional) Run domain-specific tests if relevant (e.g., `npm run test:dm:unit` for Direct Messages).
|
||||
6. Push your branch and open a Pull Request against the `main` branch.
|
||||
@@ -106,7 +107,7 @@ To set up the project locally:
|
||||
```bash
|
||||
npm run test:unit
|
||||
```
|
||||
*Note: Unit tests may take a few minutes to complete. To speed up local iterations, you can run a specific shard (e.g., `npm run test:unit:shard1`).*
|
||||
*Note: Running the full suite (`npm run test:unit`) is resource-intensive and may time out in some environments. We strongly recommend using sharded runs for local development: `npm run test:unit:shard1`.*
|
||||
|
||||
You can also run end-to-end and visual tests:
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# Automated PR Review Report
|
||||
|
||||
## 1. PR: codex/add-centralized-permission-gate
|
||||
**Target:** `main`
|
||||
**Status:** ❌ Request Changes
|
||||
|
||||
### ❌ Test Failures
|
||||
`tests/nostr-login-permissions.test.mjs` failed (6 subtests).
|
||||
> **Suggestion:** Inspect `tests/nostr-login-permissions.test.mjs`. It seems related to `extension.enable` assertions. Run `npm run test:unit` locally.
|
||||
|
||||
### 🚀 Release Channel Checks
|
||||
⚠️ **Warning:** Targets `main` but modifies critical config (`config/instance-config.js`, `js/constants.js`).
|
||||
> **Guidance:** Ensure feature flags are safe defaults.
|
||||
|
||||
### 🛡️ Guardrails
|
||||
`requires-security-review` `requires-protocol-review`
|
||||
- Touched 41 sensitive files including `js/auth/signingAdapter.js`, `js/dmDecryptor.js`, `js/nostr/nip07Permissions.js`.
|
||||
|
||||
---
|
||||
|
||||
## 2. PR: codex/remove-nip04-and-nip44-from-default-permissions
|
||||
**Target:** `main`
|
||||
**Status:** ❌ Request Changes
|
||||
|
||||
### ❌ Test Failures
|
||||
`tests/nostr-login-permissions.test.mjs` failed (6 subtests).
|
||||
> **Suggestion:** Same failure as above. Likely an environmental issue or regression in NIP-07 logic.
|
||||
|
||||
### 🚀 Release Channel Checks
|
||||
⚠️ **Warning:** Targets `main` but modifies critical config.
|
||||
|
||||
### 🛡️ Guardrails
|
||||
`requires-security-review` `requires-protocol-review`
|
||||
- Touched 41 sensitive files.
|
||||
|
||||
---
|
||||
|
||||
## 3. PR: fix/optimize-nip07-login-perf-4235940878368250470
|
||||
**Target:** `main`
|
||||
**Status:** ❌ Failure / Timeout
|
||||
|
||||
### ❌ Test Failures
|
||||
Tests timed out during execution.
|
||||
> **Suggestion:** Run `npm run test:unit:shard1` locally to verify changes without timeout.
|
||||
|
||||
---
|
||||
|
||||
## Audit Log
|
||||
| Branch | Lint | Format | Tests | Security |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| `codex/add-centralized-permission-gate` | ✅ Pass | ✅ Pass | ❌ Fail | 🛡️ Req |
|
||||
| `codex/remove-nip04-and-nip44...` | ✅ Pass | ✅ Pass | ❌ Fail | 🛡️ Req |
|
||||
| `fix/optimize-nip07-login-perf...` | ✅ Pass | ✅ Pass | ❌ Timeout | ? |
|
||||
|
||||
**Micro-fixes:** No formatting changes detected in the reviewed branches.
|
||||
@@ -180,7 +180,7 @@ See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the full developer guide.
|
||||
|
||||
**Verify your work:**
|
||||
|
||||
- **Run unit tests**: `npm run test:unit` (Required before PRs)
|
||||
- **Run unit tests**: `npm run test:unit` (Required before PRs). _Tip: Use `npm run test:unit:shard1` for faster local feedback._
|
||||
- **Format code**: `npm run format` (Required before PRs)
|
||||
- **Lint code**: `npm run lint` (Checks for CSS, hex colors, inline styles)
|
||||
|
||||
@@ -228,7 +228,7 @@ Use these artifacts to inspect the UI state at the moment of failure and diagnos
|
||||
|
||||
### Send your first video post
|
||||
|
||||
Use the event builders in `js/nostrEventSchemas.js` (the source of truth for all event definitions) to construct valid video notes.
|
||||
Use the event builders in `js/nostrEventSchemas.js` (the source of truth for all event definitions) to construct valid video notes. See [`docs/nostr-event-schemas.md`](docs/nostr-event-schemas.md) for full schema documentation.
|
||||
|
||||
```javascript
|
||||
import { buildVideoPostEvent } from "./js/nostrEventSchemas.js";
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# Daily Design System Audit Report
|
||||
|
||||
Headline: ✓ No violations
|
||||
|
||||
## CSS
|
||||
Total count: 0
|
||||
|
||||
|
||||
## Hex Colors
|
||||
Total count: 0
|
||||
|
||||
|
||||
## Inline Styles
|
||||
Total count: 0
|
||||
|
||||
|
||||
## Raw Lengths
|
||||
Total count: 0
|
||||
|
||||
|
||||
## Bracket Utilities
|
||||
Total count: 0
|
||||
|
||||
|
||||
## Tailwind Palette
|
||||
Total count: 0
|
||||
|
||||
|
||||
## Next Steps
|
||||
- [ ] Review violations
|
||||
- [ ] Apply auto-fixes (if safe)
|
||||
- [ ] Open remediation PR
|
||||
@@ -0,0 +1,111 @@
|
||||
# Telemetry Report - 2026-01-28
|
||||
|
||||
**Total Unique Issues:** 6
|
||||
**Generated:** 2026-01-28T06:17:38.223Z
|
||||
|
||||
## Top 10 Priority Issues
|
||||
|
||||
| Priority | Count | Issue | Owner | Sources |
|
||||
| :--- | :---: | :--- | :--- | :--- |
|
||||
| **High** | 6 | handleModerationBlock requests a block, clears overrides,... | QA Team | unit-test:test_unit_debug_2.log, unit-test:test_unit_debug_3.log, unit-test:test_unit_pre_commit.log, unit-test:test_unit_retry.log, unit-test:test_unit_retry_3.log, unit-test:test_unit_retry_4.log |
|
||||
| **High** | 2 | handleModerationOverride resumes deferred playback | QA Team | unit-test:test_unit_retry_2.log, unit-test:test_unit_retry_3.log |
|
||||
| **High** | 1 | handleModerationBlock requests a block, clears overrides,... | QA Team | unit-test:test_unit_debug_4.log |
|
||||
| **High** | 1 | handleModerationBlock requests a block, clears overrides,... | QA Team | unit-test:test_unit_final.log |
|
||||
| **High** | 1 | handleModerationOverride decorates stored and current vid... | QA Team | unit-test:test_unit_retry_2.log |
|
||||
| **High** | 1 | handleModerationBlock requests a block, clears overrides,... | QA Team | unit-test:test_unit_retry_2.log |
|
||||
|
||||
## Detailed Breakdown
|
||||
|
||||
### [High] handleModerationBlock requests a block, clears overrides, and refreshes hidden state
|
||||
- **Occurrences:** 6
|
||||
- **Sources:** unit-test:test_unit_debug_2.log, unit-test:test_unit_debug_3.log, unit-test:test_unit_pre_commit.log, unit-test:test_unit_retry.log, unit-test:test_unit_retry_3.log, unit-test:test_unit_retry_4.log
|
||||
- **Suggested Owner:** QA Team
|
||||
- **Fingerprint:** `56489eaa`
|
||||
|
||||
**Stack Trace / Details:**
|
||||
```
|
||||
TestContext.<anonymous> (file://$REPO/tests$REPO/moderation-overrides.test.mjs:228:10)
|
||||
async Test.run (node:internal/test_runner/test:1054:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
|
||||
...
|
||||
# Subtest: handleModerationBlock returns false when viewer is logged out
|
||||
```
|
||||
|
||||
### [High] handleModerationOverride resumes deferred playback
|
||||
- **Occurrences:** 2
|
||||
- **Sources:** unit-test:test_unit_retry_2.log, unit-test:test_unit_retry_3.log
|
||||
- **Suggested Owner:** QA Team
|
||||
- **Fingerprint:** `bcbb97f7`
|
||||
|
||||
**Stack Trace / Details:**
|
||||
```
|
||||
TestContext.<anonymous> (file://$REPO/tests$REPO/moderation-overrides.test.mjs:104:10)
|
||||
async Test.run (node:internal/test_runner/test:1054:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
|
||||
...
|
||||
# Subtest: handleModerationBlock requests a block, clears overrides, and refreshes hidden state
|
||||
```
|
||||
|
||||
### [High] handleModerationBlock requests a block, clears overrides, and refreshes hidden state
|
||||
- **Occurrences:** 1
|
||||
- **Sources:** unit-test:test_unit_debug_4.log
|
||||
- **Suggested Owner:** QA Team
|
||||
- **Fingerprint:** `80a22b69`
|
||||
|
||||
**Stack Trace / Details:**
|
||||
```
|
||||
TestContext.<anonymous> (file://$REPO/tests$REPO/moderation-overrides.test.mjs:229:10)
|
||||
async Test.run (node:internal/test_runner/test:1054:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
|
||||
...
|
||||
# Subtest: handleModerationBlock returns false when viewer is logged out
|
||||
```
|
||||
|
||||
### [High] handleModerationBlock requests a block, clears overrides, and refreshes hidden state
|
||||
- **Occurrences:** 1
|
||||
- **Sources:** unit-test:test_unit_final.log
|
||||
- **Suggested Owner:** QA Team
|
||||
- **Fingerprint:** `5e159618`
|
||||
|
||||
**Stack Trace / Details:**
|
||||
```
|
||||
TestContext.<anonymous> (file://$REPO/tests$REPO/moderation-overrides.test.mjs:232:10)
|
||||
async Test.run (node:internal/test_runner/test:1054:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
|
||||
...
|
||||
# Subtest: handleModerationBlock returns false when viewer is logged out
|
||||
```
|
||||
|
||||
### [High] handleModerationOverride decorates stored and current videos then refreshes UI
|
||||
- **Occurrences:** 1
|
||||
- **Sources:** unit-test:test_unit_retry_2.log
|
||||
- **Suggested Owner:** QA Team
|
||||
- **Fingerprint:** `4a2440b4`
|
||||
|
||||
**Stack Trace / Details:**
|
||||
```
|
||||
TestContext.<anonymous> (file://$REPO/tests$REPO/moderation-overrides.test.mjs:67:10)
|
||||
async Test.run (node:internal/test_runner/test:1054:7)
|
||||
async startSubtestAfterBootstrap (node:internal/test_runner/harness:296:3)
|
||||
...
|
||||
# Subtest: handleModerationOverride resumes deferred playback
|
||||
```
|
||||
|
||||
### [High] handleModerationBlock requests a block, clears overrides, and refreshes hidden state
|
||||
- **Occurrences:** 1
|
||||
- **Sources:** unit-test:test_unit_retry_2.log
|
||||
- **Suggested Owner:** QA Team
|
||||
- **Fingerprint:** `a879eb91`
|
||||
|
||||
**Stack Trace / Details:**
|
||||
```
|
||||
TestContext.<anonymous> (file://$REPO/tests$REPO/moderation-overrides.test.mjs:202:10)
|
||||
async Test.run (node:internal/test_runner/test:1054:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
|
||||
...
|
||||
# Subtest: handleModerationBlock returns false when viewer is logged out
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
*Privacy Notice: PII (IPs, emails, keys) has been sanitized from this report.*
|
||||
@@ -0,0 +1,74 @@
|
||||
[
|
||||
{
|
||||
"fingerprint": "56489eaa",
|
||||
"title": "handleModerationBlock requests a block, clears overrides, and refreshes hidden state",
|
||||
"count": 6,
|
||||
"sources": [
|
||||
"unit-test:test_unit_debug_2.log",
|
||||
"unit-test:test_unit_debug_3.log",
|
||||
"unit-test:test_unit_pre_commit.log",
|
||||
"unit-test:test_unit_retry.log",
|
||||
"unit-test:test_unit_retry_3.log",
|
||||
"unit-test:test_unit_retry_4.log"
|
||||
],
|
||||
"severity": "High",
|
||||
"stack": "TestContext.<anonymous> (file://$REPO/tests$REPO/moderation-overrides.test.mjs:228:10)\n async Test.run (node:internal/test_runner/test:1054:7)\n async Test.processPendingSubtests (node:internal/test_runner/test:744:7)\n ...\n# Subtest: handleModerationBlock returns false when viewer is logged out",
|
||||
"owner": "QA Team"
|
||||
},
|
||||
{
|
||||
"fingerprint": "bcbb97f7",
|
||||
"title": "handleModerationOverride resumes deferred playback",
|
||||
"count": 2,
|
||||
"sources": [
|
||||
"unit-test:test_unit_retry_2.log",
|
||||
"unit-test:test_unit_retry_3.log"
|
||||
],
|
||||
"severity": "High",
|
||||
"stack": "TestContext.<anonymous> (file://$REPO/tests$REPO/moderation-overrides.test.mjs:104:10)\n async Test.run (node:internal/test_runner/test:1054:7)\n async Test.processPendingSubtests (node:internal/test_runner/test:744:7)\n ...\n# Subtest: handleModerationBlock requests a block, clears overrides, and refreshes hidden state",
|
||||
"owner": "QA Team"
|
||||
},
|
||||
{
|
||||
"fingerprint": "80a22b69",
|
||||
"title": "handleModerationBlock requests a block, clears overrides, and refreshes hidden state",
|
||||
"count": 1,
|
||||
"sources": [
|
||||
"unit-test:test_unit_debug_4.log"
|
||||
],
|
||||
"severity": "High",
|
||||
"stack": "TestContext.<anonymous> (file://$REPO/tests$REPO/moderation-overrides.test.mjs:229:10)\n async Test.run (node:internal/test_runner/test:1054:7)\n async Test.processPendingSubtests (node:internal/test_runner/test:744:7)\n ...\n# Subtest: handleModerationBlock returns false when viewer is logged out",
|
||||
"owner": "QA Team"
|
||||
},
|
||||
{
|
||||
"fingerprint": "5e159618",
|
||||
"title": "handleModerationBlock requests a block, clears overrides, and refreshes hidden state",
|
||||
"count": 1,
|
||||
"sources": [
|
||||
"unit-test:test_unit_final.log"
|
||||
],
|
||||
"severity": "High",
|
||||
"stack": "TestContext.<anonymous> (file://$REPO/tests$REPO/moderation-overrides.test.mjs:232:10)\n async Test.run (node:internal/test_runner/test:1054:7)\n async Test.processPendingSubtests (node:internal/test_runner/test:744:7)\n ...\n# Subtest: handleModerationBlock returns false when viewer is logged out",
|
||||
"owner": "QA Team"
|
||||
},
|
||||
{
|
||||
"fingerprint": "4a2440b4",
|
||||
"title": "handleModerationOverride decorates stored and current videos then refreshes UI",
|
||||
"count": 1,
|
||||
"sources": [
|
||||
"unit-test:test_unit_retry_2.log"
|
||||
],
|
||||
"severity": "High",
|
||||
"stack": "TestContext.<anonymous> (file://$REPO/tests$REPO/moderation-overrides.test.mjs:67:10)\n async Test.run (node:internal/test_runner/test:1054:7)\n async startSubtestAfterBootstrap (node:internal/test_runner/harness:296:3)\n ...\n# Subtest: handleModerationOverride resumes deferred playback",
|
||||
"owner": "QA Team"
|
||||
},
|
||||
{
|
||||
"fingerprint": "a879eb91",
|
||||
"title": "handleModerationBlock requests a block, clears overrides, and refreshes hidden state",
|
||||
"count": 1,
|
||||
"sources": [
|
||||
"unit-test:test_unit_retry_2.log"
|
||||
],
|
||||
"severity": "High",
|
||||
"stack": "TestContext.<anonymous> (file://$REPO/tests$REPO/moderation-overrides.test.mjs:202:10)\n async Test.run (node:internal/test_runner/test:1054:7)\n async Test.processPendingSubtests (node:internal/test_runner/test:744:7)\n ...\n# Subtest: handleModerationBlock returns false when viewer is logged out",
|
||||
"owner": "QA Team"
|
||||
}
|
||||
]
|
||||
@@ -11,12 +11,12 @@
|
||||
"total": 0
|
||||
},
|
||||
"dependencies": {
|
||||
"prod": 5,
|
||||
"dev": 349,
|
||||
"optional": 29,
|
||||
"prod": 50,
|
||||
"dev": 291,
|
||||
"optional": 27,
|
||||
"peer": 0,
|
||||
"peerOptional": 0,
|
||||
"total": 353
|
||||
"total": 340
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,20 +13,6 @@
|
||||
"dependent": "app",
|
||||
"location": "/app/node_modules/esbuild"
|
||||
},
|
||||
"jsdom": {
|
||||
"current": "24.1.3",
|
||||
"wanted": "24.1.3",
|
||||
"latest": "27.4.0",
|
||||
"dependent": "app",
|
||||
"location": "/app/node_modules/jsdom"
|
||||
},
|
||||
"nostr-tools": {
|
||||
"current": "2.19.4",
|
||||
"wanted": "2.20.0",
|
||||
"latest": "2.20.0",
|
||||
"dependent": "app",
|
||||
"location": "/app/node_modules/nostr-tools"
|
||||
},
|
||||
"pixelmatch": {
|
||||
"current": "5.3.0",
|
||||
"wanted": "5.3.0",
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
js/ui/profileModalController.js:203: "Formats npub strings for display using the canonical short representation (npubXXXX…XXXX).",
|
||||
vendor/highlight.min.js:62:begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)",
|
||||
vendor/highlight.min.js:63:end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0})
|
||||
tests/visual/moderation.spec.ts:89: // TODO: Investigate why visibility check fails in fixture environment (headless)
|
||||
tests/visual/moderation.spec.ts:93: // TODO: Investigate why click times out in fixture environment (headless)
|
||||
tests/visual/moderation.spec.ts:166: // TODO: Investigate visibility check failure
|
||||
tests/visual/moderation.spec.ts:170: // TODO: Investigate click timeout failure
|
||||
tests/visual/moderation.spec.ts:219: // TODO: Investigate visibility check failure
|
||||
tests/visual/moderation.spec.ts:223: // TODO: Investigate click timeout failure
|
||||
|
||||
Note: The TODOs in tests/visual/moderation.spec.ts are already tracked in issues/todo-moderation-tests.md. No other actionable TODOs found.
|
||||
@@ -445,4 +445,4 @@ export const DEFAULT_PLAYBACK_SOURCE = "torrent";
|
||||
* source. This prevents users from staring at a loading spinner indefinitely
|
||||
* if the primary source is slow or stalled.
|
||||
*/
|
||||
export const DEFAULT_PLAYBACK_START_TIMEOUT = 2000;
|
||||
export const DEFAULT_PLAYBACK_START_TIMEOUT = 3000;
|
||||
@@ -4753,14 +4753,6 @@
|
||||
}
|
||||
|
||||
@media (min-width: theme("screens.md")) {
|
||||
.sidebar-panel__header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.sidebar-collapse-toggle {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
transform: translateX(0) !important;
|
||||
width: var(--sidebar-width);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage();
|
||||
|
||||
console.log('Navigating to index.html...');
|
||||
// We need to serve the file. I'll assume I can run this against the dev server if I start one,
|
||||
// or I can just use file:// if I build?
|
||||
// But the test uses /index.html, so it expects a server.
|
||||
// I will rely on `npm run start` being active or I need to start it.
|
||||
|
||||
// Since I cannot easily start the server and keep it running in this environment without blocking,
|
||||
// I will assume the issue is static analysis or I need to fix it blindly.
|
||||
|
||||
// However, I can inspect the CSS file content I read.
|
||||
|
||||
await browser.close();
|
||||
})();
|
||||
@@ -341,11 +341,11 @@ valid events:
|
||||
|
||||
```bash
|
||||
# Run the validation script
|
||||
node scripts/agent/validate-events.mjs
|
||||
node scripts/agent/validate-events.test.mjs
|
||||
|
||||
# Run the unit test suite
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
When modifying schemas or builders, use `scripts/agent/validate-events.mjs` to
|
||||
When modifying schemas or builders, use `scripts/agent/validate-events.test.mjs` to
|
||||
verify your changes locally.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Onboarding Audit Report - 2026-01-28
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This report documents the results of a "fresh checkout" onboarding audit for the bitvid repository. The audit simulated the experience of a new developer setting up the project for the first time, following the instructions in `README.md` and `CONTRIBUTING.md`.
|
||||
|
||||
**Overall Status**: ✅ Success (with minor adjustments)
|
||||
|
||||
All core onboarding steps (`npm ci`, `npm run build`, `npm run format`) completed successfully. One build warning was identified and resolved. Documentation was updated to better manage expectations regarding unit test execution times.
|
||||
|
||||
## Audit Environment
|
||||
|
||||
- **Date**: 2026-01-28
|
||||
- **OS/Environment**: Linux (Simulated CI/Sandboxed Environment)
|
||||
- **Node Version**: v22 (matching `.devcontainer` and `package.json` expectations)
|
||||
|
||||
## Step-by-Step Verification
|
||||
|
||||
### 1. Dependency Installation
|
||||
**Command**: `npm ci`
|
||||
**Result**: ✅ Success
|
||||
- Installed 314 packages.
|
||||
- No vulnerabilities found.
|
||||
|
||||
### 2. Build Process
|
||||
**Command**: `npm run build` (alias for `npm run build:css`)
|
||||
**Result**: ⚠️ Success with Warning (Resolved)
|
||||
- **Initial Outcome**: The build succeeded, but Tailwind emitted a warning:
|
||||
> `warn - The min-* and max-* variants are not supported with a screens configuration containing mixed units.`
|
||||
- **Root Cause**: `tailwind.config.cjs` defined custom screens using `rem` units (`xs: "30rem"`, `compact: "25rem"`) while extending the default `px`-based configuration.
|
||||
- **Fix Applied**: Converted custom screen definitions to pixels (`xs: "480px"`, `compact: "400px"`) in `tailwind.config.cjs`.
|
||||
- **Verification**: Re-running `npm run build:css` produced a clean output with no warnings.
|
||||
|
||||
### 3. Formatting
|
||||
**Command**: `npm run format`
|
||||
**Result**: ✅ Success
|
||||
- Prettier ran successfully on CSS, tokens, HTML, and Markdown files.
|
||||
|
||||
### 4. Unit Tests
|
||||
**Command**: `npm run test:unit`
|
||||
**Result**: ⚠️ Timeout (Expected/Known Issue)
|
||||
- **Observation**: The full unit test suite timed out in the resource-constrained test environment.
|
||||
- **Action**: Updated `CONTRIBUTING.md` to explicitly recommend running tests in shards (e.g., `npm run test:unit:shard1`) for local development, consistent with CI practices.
|
||||
|
||||
## Dev Container Verification
|
||||
|
||||
The repository includes a `.devcontainer` configuration that appears robust and follows best practices:
|
||||
- **Base Image**: `mcr.microsoft.com/devcontainers/javascript-node:22` (Matches project Node requirement).
|
||||
- **Features**: Includes GitHub CLI.
|
||||
- **Post-Create Command**: Automates `npm ci`, `npm run build`, and `npx playwright install`.
|
||||
- **Extensions**: Pre-installs relevant VS Code extensions (Prettier, Tailwind, Stylelint).
|
||||
|
||||
**Conclusion**: No changes were needed for the devcontainer configuration.
|
||||
|
||||
## Summary of Changes
|
||||
|
||||
1. **Code**: Modified `tailwind.config.cjs` to use consistent pixel units for screen breakpoints, eliminating build warnings.
|
||||
2. **Documentation**: Updated `CONTRIBUTING.md` to:
|
||||
- Warn about potential timeouts when running the full unit test suite.
|
||||
- Recommend `npm run test:unit:shard1` as a faster alternative for local verification.
|
||||
- Update the PR submission checklist to reflect these testing options.
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"target": "nostrEventSchemas",
|
||||
"timestamp": "2026-01-28T16:18:44.207Z",
|
||||
"error": {
|
||||
"message": "hexPubkeys.forEach is not a function",
|
||||
"stack": "TypeError: hexPubkeys.forEach is not a function\n at Module.buildAdminListEvent (file:///app/js/nostrEventSchemas.js:2535:14)\n at fuzzTest (file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:232:17)\n at runFuzzer (file:///app/scripts/agent/fuzz-shared.mjs:164:21)",
|
||||
"name": "TypeError"
|
||||
},
|
||||
"input": null
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Reproducer for Moderation UI Visibility Bug
|
||||
|
||||
This reproducer demonstrates the issue where the "Show anyway" button and related moderation badges fail to render or be visible in the test environment (headless mode), despite logic suggesting they should be present.
|
||||
|
||||
## Issue Description
|
||||
In `tests/visual/moderation.spec.ts`, several assertions are commented out because `expect(showAnywayButton).toBeVisible()` fails or clicking the button times out.
|
||||
|
||||
## How to Run
|
||||
|
||||
1. Build the project (specifically CSS):
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
2. Run the reproducer test (using the repro-specific config):
|
||||
```bash
|
||||
npx playwright test -c examples/reproducers/playwright.config.ts examples/reproducers/issue-moderation-visibility/repro.spec.ts
|
||||
```
|
||||
|
||||
## Expected Outcome
|
||||
The test should fail at `await expect(showAnywayButton).toBeVisible();` or timeout.
|
||||
@@ -0,0 +1,76 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
const FIXTURE_URL = "/docs/moderation/fixtures/index.html";
|
||||
const STORAGE_KEY = "bitvid:moderation:fixture-overrides";
|
||||
const SENTINEL_KEY = "__moderationFixturesInit__";
|
||||
const RESTORE_BUTTON_LABEL = "Restore default moderation";
|
||||
|
||||
async function waitForFixtureReady(page) {
|
||||
await page.waitForSelector('body[data-ready="true"]');
|
||||
}
|
||||
|
||||
function setupStorageReset(page) {
|
||||
return page.addInitScript(({ storageKey, sentinelKey }) => {
|
||||
try {
|
||||
const session = window.sessionStorage;
|
||||
const local = window.localStorage;
|
||||
if (session && !session.getItem(sentinelKey)) {
|
||||
local?.removeItem(storageKey);
|
||||
session.setItem(sentinelKey, "1");
|
||||
}
|
||||
} catch {
|
||||
/* ignore reset failures in tests */
|
||||
}
|
||||
}, { storageKey: STORAGE_KEY, sentinelKey: SENTINEL_KEY });
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setupStorageReset(page);
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
try {
|
||||
await page.evaluate(({ key, sentinel }) => {
|
||||
window.localStorage?.removeItem(key);
|
||||
window.sessionStorage?.removeItem(sentinel);
|
||||
}, { key: STORAGE_KEY, sentinel: SENTINEL_KEY });
|
||||
} catch {
|
||||
// ignore cleanup errors when page already closed
|
||||
}
|
||||
});
|
||||
|
||||
test("show anyway override persists across reloads (repro)", async ({ page }) => {
|
||||
await page.goto(FIXTURE_URL, { waitUntil: "networkidle" });
|
||||
await waitForFixtureReady(page);
|
||||
|
||||
const overrideCard = page.locator('[data-test-id="show-anyway"]');
|
||||
const thumbnail = overrideCard.locator('img[data-video-thumbnail]');
|
||||
|
||||
await expect(overrideCard).toHaveAttribute(
|
||||
"data-moderation-override-available",
|
||||
"true"
|
||||
);
|
||||
const showAnywayButton = overrideCard.getByRole("button", { name: "Show anyway" });
|
||||
const restoreButtonQuery = overrideCard.getByRole("button", {
|
||||
name: RESTORE_BUTTON_LABEL,
|
||||
});
|
||||
|
||||
await expect(overrideCard).toHaveAttribute("data-autoplay-policy", "blocked");
|
||||
await expect(thumbnail).toHaveAttribute("data-thumbnail-state", "blurred");
|
||||
|
||||
// UNCOMMENTED FAILING ASSERTIONS
|
||||
console.log("Checking visibility of 'Show anyway' button...");
|
||||
await expect(showAnywayButton).toBeVisible();
|
||||
|
||||
await expect(restoreButtonQuery).toHaveCount(0);
|
||||
|
||||
console.log("Clicking 'Show anyway' button...");
|
||||
await showAnywayButton.click({ force: true });
|
||||
|
||||
await expect(overrideCard).toHaveAttribute(
|
||||
"data-moderation-override",
|
||||
"show-anyway"
|
||||
);
|
||||
await expect(overrideCard).not.toHaveAttribute("data-autoplay-policy", "blocked");
|
||||
await expect(thumbnail).not.toHaveAttribute("data-thumbnail-state", "blurred");
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
# Reproducer for Popover Viewport Bug
|
||||
|
||||
This reproducer demonstrates the flaky issue where the popover fails to flip to the top in a restricted viewport.
|
||||
|
||||
## Issue Description
|
||||
In `tests/e2e/popover.spec.ts`, the test "keeps the bottom-right grid menu inside the viewport" is skipped because it is flaky or fails. This script unskips it to reproduce the failure.
|
||||
|
||||
## How to Run
|
||||
|
||||
1. Build the project (specifically CSS):
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
2. Run the reproducer test (using the repro-specific config):
|
||||
```bash
|
||||
npx playwright test -c examples/reproducers/playwright.config.ts examples/reproducers/issue-popover-viewport/repro.spec.ts
|
||||
```
|
||||
|
||||
## Expected Outcome
|
||||
The test might fail with a placement assertion error (expected 'top...', got 'bottom...') or similar.
|
||||
@@ -0,0 +1,81 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import type { Locator } from "@playwright/test";
|
||||
import {
|
||||
applyReducedMotion,
|
||||
failOnConsoleErrors,
|
||||
} from "../../../tests/e2e/helpers/uiTestUtils";
|
||||
|
||||
async function getPanelMetrics(locator: Locator) {
|
||||
return locator.evaluate((node) => {
|
||||
const rect = node.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
const tokenValue = window
|
||||
.getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--popover-inline-safe-max")
|
||||
.trim();
|
||||
const popoverMaxWidth = window
|
||||
.getComputedStyle(node)
|
||||
.getPropertyValue("--popover-max-width")
|
||||
.trim();
|
||||
|
||||
return {
|
||||
rect: {
|
||||
top: rect.top,
|
||||
left: rect.left,
|
||||
right: rect.right,
|
||||
bottom: rect.bottom,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
},
|
||||
viewport: { width: viewportWidth, height: viewportHeight },
|
||||
placement: node.dataset.popoverPlacement || "",
|
||||
state: node.dataset.popoverState || "",
|
||||
popoverMaxWidth,
|
||||
tokenMaxWidth: tokenValue,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function assertWithinViewport(metrics: {
|
||||
rect: { top: number; left: number; right: number; bottom: number };
|
||||
viewport: { width: number; height: number };
|
||||
}) {
|
||||
const tolerance = 0.5;
|
||||
expect(metrics.rect.left).toBeGreaterThanOrEqual(-tolerance);
|
||||
expect(metrics.rect.top).toBeGreaterThanOrEqual(-tolerance);
|
||||
expect(metrics.rect.right).toBeLessThanOrEqual(metrics.viewport.width + tolerance);
|
||||
expect(metrics.rect.bottom).toBeLessThanOrEqual(metrics.viewport.height + tolerance);
|
||||
}
|
||||
|
||||
test.describe("popover layout scenarios (repro)", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await applyReducedMotion(page);
|
||||
failOnConsoleErrors(page);
|
||||
await page.goto("/docs/popover-scenarios.html", { waitUntil: "networkidle" });
|
||||
});
|
||||
|
||||
// UN-SKIPPED TEST
|
||||
test("keeps the bottom-right grid menu inside the viewport", async ({ page }) => {
|
||||
// Force a tall body to ensure there is space for the popover to flip to the top
|
||||
await page.addStyleTag({ content: "body { min-height: 2000px !important; }" });
|
||||
// Reduce viewport height to force the menu to flip to the top
|
||||
await page.setViewportSize({ width: 1280, height: 250 });
|
||||
|
||||
const trigger = page.locator('[data-test-trigger="grid-bottom-right"]');
|
||||
// Scroll to bottom to ensure the element is near the viewport edge
|
||||
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
||||
|
||||
await trigger.click();
|
||||
const panel = page.locator('[data-test-panel="grid-bottom-right"]');
|
||||
await expect(panel).toHaveAttribute("data-popover-state", "open");
|
||||
|
||||
const metrics = await getPanelMetrics(panel);
|
||||
|
||||
assertWithinViewport(metrics);
|
||||
expect(metrics.state).toBe("open");
|
||||
expect(metrics.placement.startsWith("top"), `Expected placement to start with 'top' but got '${metrics.placement}'`).toBeTruthy();
|
||||
expect(metrics.popoverMaxWidth).toBe(metrics.tokenMaxWidth);
|
||||
expect(metrics.tokenMaxWidth.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,24 @@
|
||||
import { defineConfig } from "@playwright/test";
|
||||
import baseConfig from "../../playwright.config";
|
||||
|
||||
const HOST = process.env.PLAYWRIGHT_HOST ?? "127.0.0.1";
|
||||
const PORT = Number.parseInt(process.env.PLAYWRIGHT_PORT ?? "4173", 10);
|
||||
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL ?? `http://${HOST}:${PORT}`;
|
||||
|
||||
export default defineConfig({
|
||||
...baseConfig,
|
||||
testDir: ".",
|
||||
projects: [
|
||||
{
|
||||
name: "repro",
|
||||
testMatch: "**/*.spec.ts",
|
||||
},
|
||||
],
|
||||
outputDir: "../../artifacts/test-results-repro",
|
||||
timeout: 60_000,
|
||||
reporter: [["list"], ["html", { open: "never" }]],
|
||||
use: {
|
||||
baseURL: BASE_URL,
|
||||
viewport: { width: 1280, height: 720 },
|
||||
ignoreHTTPSErrors: true,
|
||||
},
|
||||
webServer: {
|
||||
// Config is in examples/reproducers, so cwd is there. We need to serve the repo root.
|
||||
command: `npx http-server ../.. -p ${PORT} -a ${HOST} -c-1 --silent`,
|
||||
url: `${BASE_URL}/docs/kitchen-sink.html`,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120_000,
|
||||
},
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
# Upgrade jsdom
|
||||
|
||||
- **Current Version:** 24.1.3
|
||||
- **Latest Version:** 27.4.0
|
||||
|
||||
**Status:** Major Upgrade Required
|
||||
**Reason:** Major version bump available. Requires strict testing and separate PR.
|
||||
@@ -3271,6 +3271,19 @@ class Application {
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.nostrService.loadDirectMessages({
|
||||
actorPubkey: activePubkey,
|
||||
limit: 50,
|
||||
initialLoad: true,
|
||||
});
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[Application] Failed to sync direct messages during login:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -4097,6 +4110,15 @@ class Application {
|
||||
this.resetViewLoggingState();
|
||||
}
|
||||
|
||||
// Stop existing feed subscription to prioritize user data sync
|
||||
if (
|
||||
this.videoSubscription &&
|
||||
typeof this.videoSubscription.unsub === "function"
|
||||
) {
|
||||
this.videoSubscription.unsub();
|
||||
this.videoSubscription = null;
|
||||
}
|
||||
|
||||
this.applyAuthenticatedUiState();
|
||||
this.commentController?.refreshAuthState?.();
|
||||
this.updateShareNostrAuthState({ reason: "auth-login" });
|
||||
@@ -4156,7 +4178,12 @@ class Application {
|
||||
|
||||
const hashtagPreferencesPromise = Promise.resolve(
|
||||
this.loadHashtagPreferencesForPubkey(loginContext.pubkey),
|
||||
);
|
||||
).catch((error) => {
|
||||
devLogger.warn(
|
||||
"[Application] Background hashtag preferences load failed:",
|
||||
error
|
||||
);
|
||||
});
|
||||
|
||||
if (this.zapController) {
|
||||
try {
|
||||
@@ -4244,7 +4271,7 @@ class Application {
|
||||
}
|
||||
|
||||
await nwcPromise;
|
||||
await hashtagPreferencesPromise;
|
||||
// We do not await hashtagPreferencesPromise here; it loads in the background.
|
||||
|
||||
try {
|
||||
await accessControl.ensureReady();
|
||||
@@ -4257,28 +4284,54 @@ class Application {
|
||||
|
||||
if (activePubkey) {
|
||||
const aggregatedBlacklist = accessControl.getBlacklist();
|
||||
try {
|
||||
await userBlocks.seedWithNpubs(
|
||||
|
||||
// Background the seeding process because it involves publishing events,
|
||||
// which can block the login flow significantly.
|
||||
userBlocks
|
||||
.seedWithNpubs(
|
||||
activePubkey,
|
||||
Array.isArray(aggregatedBlacklist) ? aggregatedBlacklist : [],
|
||||
);
|
||||
Array.isArray(aggregatedBlacklist) ? aggregatedBlacklist : []
|
||||
)
|
||||
.catch((error) => {
|
||||
if (
|
||||
error?.code === "extension-permission-denied" ||
|
||||
error?.code === "nip04-missing" ||
|
||||
error?.name === "RelayPublishError"
|
||||
) {
|
||||
userLogger.error(
|
||||
"[Application] Failed to seed shared block list after login:",
|
||||
error
|
||||
);
|
||||
} else {
|
||||
devLogger.error(
|
||||
"[Application] Unexpected error while seeding shared block list:",
|
||||
error
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Attempt to load blocks with a short timeout so we don't block the UI forever.
|
||||
// If it times out, the UI will refresh reactively when the data arrives.
|
||||
const blockLoadPromise = userBlocks.ensureLoaded(activePubkey);
|
||||
const blockLoadTimeout = new Promise((resolve) =>
|
||||
setTimeout(resolve, 2000)
|
||||
);
|
||||
try {
|
||||
await Promise.race([blockLoadPromise, blockLoadTimeout]);
|
||||
} catch (error) {
|
||||
if (
|
||||
error?.code === "extension-permission-denied" ||
|
||||
error?.code === "nip04-missing" ||
|
||||
error?.name === "RelayPublishError"
|
||||
) {
|
||||
userLogger.error(
|
||||
"[Application] Failed to seed shared block list after login:",
|
||||
error,
|
||||
);
|
||||
} else {
|
||||
devLogger.error(
|
||||
"[Application] Unexpected error while seeding shared block list:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
devLogger.warn(
|
||||
"[Application] Block list load timed out or failed:",
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
// Load subscriptions in the background to avoid blocking the main feed.
|
||||
subscriptions.ensureLoaded(activePubkey).catch((error) => {
|
||||
devLogger.warn(
|
||||
"[Application] Failed to load subscriptions during login:",
|
||||
error
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { signEventWithPrivateKey } from "../nostr/publishHelpers.js";
|
||||
import { ensureNostrTools, getCachedNostrTools } from "../nostr/toolkit.js";
|
||||
import { normalizeActorKey } from "../nostr/watchHistory.js";
|
||||
import { buildHttpAuthEvent } from "../nostrEventSchemas.js";
|
||||
import { runNip07WithRetry } from "../nostr/nip07Permissions.js";
|
||||
|
||||
const DEFAULT_TEST_PUBKEY = "f".repeat(64);
|
||||
const DEFAULT_DEV_DISPLAY_NAME = "Ephemeral dev signer";
|
||||
@@ -31,22 +32,37 @@ export function createNip07SigningAdapter({ extension } = {}) {
|
||||
(typeof window !== "undefined" && window?.nostr ? window.nostr : null);
|
||||
|
||||
const getPubkey = async () => {
|
||||
if (!resolvedExtension || typeof resolvedExtension.getPublicKey !== "function") {
|
||||
if (
|
||||
!resolvedExtension ||
|
||||
typeof resolvedExtension.getPublicKey !== "function"
|
||||
) {
|
||||
throw new Error("NIP-07 extension is unavailable.");
|
||||
}
|
||||
return normalizePubkey(await resolvedExtension.getPublicKey());
|
||||
return normalizePubkey(
|
||||
await runNip07WithRetry(() => resolvedExtension.getPublicKey(), {
|
||||
label: "extension.getPublicKey",
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const getDisplayName = async () => {
|
||||
if (!resolvedExtension || typeof resolvedExtension.getMetadata !== "function") {
|
||||
if (
|
||||
!resolvedExtension ||
|
||||
typeof resolvedExtension.getMetadata !== "function"
|
||||
) {
|
||||
return "";
|
||||
}
|
||||
const metadata = await resolvedExtension.getMetadata();
|
||||
const metadata = await runNip07WithRetry(
|
||||
() => resolvedExtension.getMetadata(),
|
||||
{ label: "extension.getMetadata" },
|
||||
);
|
||||
if (!metadata || typeof metadata !== "object") {
|
||||
return "";
|
||||
}
|
||||
const displayName =
|
||||
typeof metadata.display_name === "string" ? metadata.display_name.trim() : "";
|
||||
typeof metadata.display_name === "string"
|
||||
? metadata.display_name.trim()
|
||||
: "";
|
||||
if (displayName) {
|
||||
return displayName;
|
||||
}
|
||||
@@ -54,17 +70,27 @@ export function createNip07SigningAdapter({ extension } = {}) {
|
||||
};
|
||||
|
||||
const signEvent = async (event) => {
|
||||
if (!resolvedExtension || typeof resolvedExtension.signEvent !== "function") {
|
||||
if (
|
||||
!resolvedExtension ||
|
||||
typeof resolvedExtension.signEvent !== "function"
|
||||
) {
|
||||
throw new Error("NIP-07 extension is missing signEvent.");
|
||||
}
|
||||
return resolvedExtension.signEvent(event);
|
||||
return runNip07WithRetry(() => resolvedExtension.signEvent(event), {
|
||||
label: "extension.signEvent",
|
||||
});
|
||||
};
|
||||
|
||||
const signMessage = async (message) => {
|
||||
if (!resolvedExtension || typeof resolvedExtension.signMessage !== "function") {
|
||||
if (
|
||||
!resolvedExtension ||
|
||||
typeof resolvedExtension.signMessage !== "function"
|
||||
) {
|
||||
throw new Error("NIP-07 extension does not support signMessage.");
|
||||
}
|
||||
return resolvedExtension.signMessage(message);
|
||||
return runNip07WithRetry(() => resolvedExtension.signMessage(message), {
|
||||
label: "extension.signMessage",
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
import { normalizeActorKey } from "../watchHistory.js";
|
||||
import { runNip07WithRetry } from "../nip07Permissions.js";
|
||||
|
||||
async function retryNip07Call(operation, label) {
|
||||
async function retryNip07Call(operation, label, priority) {
|
||||
let lastError = null;
|
||||
const attempts = 2; // Retry once on failure
|
||||
for (let i = 0; i < attempts; i++) {
|
||||
try {
|
||||
return await runNip07WithRetry(operation, { label });
|
||||
return await runNip07WithRetry(operation, { label, priority });
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
// Don't retry if user explicitly rejected
|
||||
@@ -97,40 +97,56 @@ export async function createNip07Adapter(initialExtension) {
|
||||
const hasNip44 = !!bootstrapNip44;
|
||||
|
||||
// Wrapper builders to ensure dynamic lookup at call time
|
||||
const createNip04Encrypt = () => (pubkey, plaintext) => {
|
||||
const createNip04Encrypt = () => (pubkey, plaintext, options = {}) => {
|
||||
const ext = getExtension();
|
||||
const nip04 = ext?.nip04;
|
||||
if (!nip04 || typeof nip04.encrypt !== "function") {
|
||||
throw new Error("NIP-04 encryption unavailable on active extension.");
|
||||
}
|
||||
return retryNip07Call(() => nip04.encrypt(pubkey, plaintext), "nip04.encrypt");
|
||||
return retryNip07Call(
|
||||
() => nip04.encrypt(pubkey, plaintext),
|
||||
"nip04.encrypt",
|
||||
options?.priority,
|
||||
);
|
||||
};
|
||||
|
||||
const createNip04Decrypt = () => (pubkey, ciphertext) => {
|
||||
const createNip04Decrypt = () => (pubkey, ciphertext, options = {}) => {
|
||||
const ext = getExtension();
|
||||
const nip04 = ext?.nip04;
|
||||
if (!nip04 || typeof nip04.decrypt !== "function") {
|
||||
throw new Error("NIP-04 decryption unavailable on active extension.");
|
||||
}
|
||||
return retryNip07Call(() => nip04.decrypt(pubkey, ciphertext), "nip04.decrypt");
|
||||
return retryNip07Call(
|
||||
() => nip04.decrypt(pubkey, ciphertext),
|
||||
"nip04.decrypt",
|
||||
options?.priority,
|
||||
);
|
||||
};
|
||||
|
||||
const createNip44Encrypt = () => (pubkey, plaintext) => {
|
||||
const createNip44Encrypt = () => (pubkey, plaintext, options = {}) => {
|
||||
const ext = getExtension();
|
||||
const nip44 = resolveNip44Module(ext);
|
||||
if (!nip44 || typeof nip44.encrypt !== "function") {
|
||||
throw new Error("NIP-44 encryption unavailable on active extension.");
|
||||
}
|
||||
return retryNip07Call(() => nip44.encrypt(pubkey, plaintext), "nip44.encrypt");
|
||||
return retryNip07Call(
|
||||
() => nip44.encrypt(pubkey, plaintext),
|
||||
"nip44.encrypt",
|
||||
options?.priority,
|
||||
);
|
||||
};
|
||||
|
||||
const createNip44Decrypt = () => (pubkey, ciphertext) => {
|
||||
const createNip44Decrypt = () => (pubkey, ciphertext, options = {}) => {
|
||||
const ext = getExtension();
|
||||
const nip44 = resolveNip44Module(ext);
|
||||
if (!nip44 || typeof nip44.decrypt !== "function") {
|
||||
throw new Error("NIP-44 decryption unavailable on active extension.");
|
||||
}
|
||||
return retryNip07Call(() => nip44.decrypt(pubkey, ciphertext), "nip44.decrypt");
|
||||
return retryNip07Call(
|
||||
() => nip44.decrypt(pubkey, ciphertext),
|
||||
"nip44.decrypt",
|
||||
options?.priority,
|
||||
);
|
||||
};
|
||||
|
||||
const requestPermissions = async (methods = []) => {
|
||||
|
||||
@@ -65,16 +65,20 @@ export async function createNip46Adapter(client) {
|
||||
};
|
||||
|
||||
if (typeof resolvedClient.nip04Encrypt === "function") {
|
||||
signer.nip04Encrypt = resolvedClient.nip04Encrypt.bind(resolvedClient);
|
||||
signer.nip04Encrypt = (pubkey, plaintext, options) =>
|
||||
resolvedClient.nip04Encrypt(pubkey, plaintext, options);
|
||||
}
|
||||
if (typeof resolvedClient.nip04Decrypt === "function") {
|
||||
signer.nip04Decrypt = resolvedClient.nip04Decrypt.bind(resolvedClient);
|
||||
signer.nip04Decrypt = (pubkey, ciphertext, options) =>
|
||||
resolvedClient.nip04Decrypt(pubkey, ciphertext, options);
|
||||
}
|
||||
if (typeof resolvedClient.nip44Encrypt === "function") {
|
||||
signer.nip44Encrypt = resolvedClient.nip44Encrypt.bind(resolvedClient);
|
||||
signer.nip44Encrypt = (pubkey, plaintext, options) =>
|
||||
resolvedClient.nip44Encrypt(pubkey, plaintext, options);
|
||||
}
|
||||
if (typeof resolvedClient.nip44Decrypt === "function") {
|
||||
signer.nip44Decrypt = resolvedClient.nip44Decrypt.bind(resolvedClient);
|
||||
signer.nip44Decrypt = (pubkey, ciphertext, options) =>
|
||||
resolvedClient.nip44Decrypt(pubkey, ciphertext, options);
|
||||
}
|
||||
|
||||
signer.capabilities.nip04 = Boolean(
|
||||
|
||||
+219
-3
@@ -19,7 +19,6 @@
|
||||
|
||||
import { isDevMode } from "../config.js";
|
||||
import { FEATURE_PUBLISH_NIP71 } from "../constants.js";
|
||||
import { accessControl } from "../accessControl.js";
|
||||
import { bytesToHex, sha256 } from "../../vendor/crypto-helpers.bundle.min.js";
|
||||
import { infoHashFromMagnet } from "../magnets.js";
|
||||
// 🔧 merged conflicting changes from codex/update-video-publishing-and-parsing-logic vs unstable
|
||||
@@ -140,6 +139,7 @@ import {
|
||||
import {
|
||||
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
||||
DEFAULT_NIP07_PERMISSION_METHODS,
|
||||
NIP07_PRIORITY,
|
||||
clearStoredNip07Permissions,
|
||||
normalizePermissionMethod,
|
||||
readStoredNip07Permissions,
|
||||
@@ -1900,6 +1900,157 @@ export class NostrClient {
|
||||
* 4. **Authorize**: Handles any `auth_url` challenges if the signer requires out-of-band approval.
|
||||
* 5. **Session**: Stores the session metadata locally (if `remember: true`) to allow auto-reconnect.
|
||||
*/
|
||||
async loginWithExtension(options = {}) {
|
||||
let extension = null;
|
||||
let attempts = 0;
|
||||
const maxAttempts = 3;
|
||||
|
||||
// Optimistic check: if window.nostr is already there, use it immediately.
|
||||
if (typeof window !== "undefined" && window.nostr) {
|
||||
extension = window.nostr;
|
||||
} else {
|
||||
while (!extension && attempts < maxAttempts) {
|
||||
try {
|
||||
// Fast wait for the first attempt, slightly longer for retries
|
||||
const timeout = attempts === 0 ? 1500 : 3000;
|
||||
await waitForNip07Extension(timeout);
|
||||
extension = window.nostr;
|
||||
} catch (waitError) {
|
||||
devLogger.log(
|
||||
`Timed out waiting for extension injection (attempt ${
|
||||
attempts + 1
|
||||
}/${maxAttempts}):`,
|
||||
waitError,
|
||||
);
|
||||
}
|
||||
|
||||
if (extension) {
|
||||
break;
|
||||
}
|
||||
|
||||
attempts++;
|
||||
if (attempts < maxAttempts) {
|
||||
devLogger.log("Retrying NIP-07 detection...");
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!extension) {
|
||||
devLogger.log("No Nostr extension found");
|
||||
throw new Error("Please install a Nostr extension (Alby, nos2x, etc.).");
|
||||
}
|
||||
|
||||
const { allowAccountSelection = false, expectPubkey } =
|
||||
typeof options === "object" && options !== null ? options : {};
|
||||
const normalizedExpectedPubkey =
|
||||
typeof expectPubkey === "string" && expectPubkey.trim()
|
||||
? expectPubkey.trim().toLowerCase()
|
||||
: null;
|
||||
|
||||
if (typeof extension.getPublicKey !== "function") {
|
||||
throw new Error(
|
||||
"This NIP-07 extension is missing getPublicKey support. Please update the extension.",
|
||||
);
|
||||
}
|
||||
|
||||
const permissionResult = await this.ensureExtensionPermissions(
|
||||
DEFAULT_NIP07_PERMISSION_METHODS,
|
||||
);
|
||||
if (!permissionResult.ok) {
|
||||
const denialMessage =
|
||||
'The NIP-07 extension reported "permission denied". Please approve the prompt and try again.';
|
||||
const denialError = new Error(denialMessage);
|
||||
if (permissionResult.error) {
|
||||
denialError.cause = permissionResult.error;
|
||||
}
|
||||
throw denialError;
|
||||
}
|
||||
|
||||
if (
|
||||
allowAccountSelection &&
|
||||
typeof extension.selectAccounts === "function"
|
||||
) {
|
||||
try {
|
||||
const selection = await runNip07WithRetry(
|
||||
() =>
|
||||
extension.selectAccounts(expectPubkey ? [expectPubkey] : undefined),
|
||||
{ label: "extension.selectAccounts" },
|
||||
);
|
||||
|
||||
const didCancelSelection =
|
||||
selection === undefined ||
|
||||
selection === null ||
|
||||
selection === false ||
|
||||
(Array.isArray(selection) && selection.length === 0);
|
||||
|
||||
if (didCancelSelection) {
|
||||
throw new Error("Account selection was cancelled.");
|
||||
}
|
||||
} catch (selectionErr) {
|
||||
const message =
|
||||
selectionErr && typeof selectionErr.message === "string"
|
||||
? selectionErr.message
|
||||
: "Account selection was cancelled.";
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
const pubkey = await runNip07WithRetry(() => extension.getPublicKey(), {
|
||||
label: "extension.getPublicKey",
|
||||
});
|
||||
|
||||
if (!pubkey || typeof pubkey !== "string") {
|
||||
throw new Error(
|
||||
"The NIP-07 extension did not return a public key. Please try again.",
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
normalizedExpectedPubkey &&
|
||||
pubkey.toLowerCase() !== normalizedExpectedPubkey
|
||||
) {
|
||||
throw new Error(
|
||||
"The selected account doesn't match the expected profile. Please try again.",
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof options.validator === "function") {
|
||||
try {
|
||||
const isValid = await options.validator(pubkey);
|
||||
if (!isValid) {
|
||||
throw new Error("Access denied.");
|
||||
}
|
||||
} catch (validationError) {
|
||||
const message =
|
||||
validationError instanceof Error
|
||||
? validationError.message
|
||||
: "Access denied.";
|
||||
const error = new Error(message);
|
||||
error.code = "access-denied";
|
||||
if (validationError instanceof Error) {
|
||||
error.cause = validationError;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const adapter = await createNip07Adapter(extension);
|
||||
adapter.pubkey = pubkey;
|
||||
setActiveSigner(adapter);
|
||||
|
||||
this.ensureExtensionPermissions(DEFAULT_NIP07_PERMISSION_METHODS).catch(
|
||||
(err) => {
|
||||
userLogger.warn(
|
||||
"[nostr] Extension permissions were not fully granted after login:",
|
||||
err,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return { pubkey, signer: adapter };
|
||||
}
|
||||
|
||||
async connectRemoteSigner({
|
||||
connectionString,
|
||||
remember = true,
|
||||
@@ -2201,6 +2352,25 @@ export class NostrClient {
|
||||
remotePubkey: summarizeHexForLog(remotePubkey),
|
||||
userPubkey: summarizeHexForLog(userPubkey),
|
||||
});
|
||||
|
||||
if (typeof options?.validator === "function") {
|
||||
try {
|
||||
const isValid = await options.validator(userPubkey);
|
||||
if (!isValid) {
|
||||
throw new Error("Access denied.");
|
||||
}
|
||||
} catch (validationError) {
|
||||
const message =
|
||||
validationError instanceof Error
|
||||
? validationError.message
|
||||
: "Access denied.";
|
||||
const error = new Error(message);
|
||||
error.code = "access-denied";
|
||||
client.destroy(); // Cleanup client since we won't use it
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
client.metadata = metadata;
|
||||
const signer = await this.installNip46Client(client, { userPubkey });
|
||||
|
||||
@@ -2296,6 +2466,7 @@ export class NostrClient {
|
||||
options && typeof options === "object" ? options : {};
|
||||
const silent = normalizedOptions.silent === true;
|
||||
const forgetOnError = normalizedOptions.forgetOnError === true;
|
||||
const validator = typeof normalizedOptions.validator === "function" ? normalizedOptions.validator : null;
|
||||
|
||||
const stored = readStoredNip46Session();
|
||||
if (!stored) {
|
||||
@@ -2344,6 +2515,25 @@ export class NostrClient {
|
||||
await client.ensureSubscription();
|
||||
await client.connect({ permissions: stored.permissions });
|
||||
const userPubkey = await client.getUserPubkey();
|
||||
|
||||
if (validator) {
|
||||
try {
|
||||
const isValid = await validator(userPubkey);
|
||||
if (!isValid) {
|
||||
throw new Error("Access denied.");
|
||||
}
|
||||
} catch (validationError) {
|
||||
const message =
|
||||
validationError instanceof Error
|
||||
? validationError.message
|
||||
: "Access denied.";
|
||||
const error = new Error(message);
|
||||
error.code = "access-denied";
|
||||
client.destroy();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
client.metadata = stored.metadata;
|
||||
const signer = await this.installNip46Client(client, { userPubkey });
|
||||
|
||||
@@ -3122,6 +3312,7 @@ export class NostrClient {
|
||||
pubkey,
|
||||
persist = false,
|
||||
passphrase,
|
||||
validator,
|
||||
} = {}) {
|
||||
const normalizedPrivateKey =
|
||||
typeof privateKey === "string" && HEX64_REGEX.test(privateKey)
|
||||
@@ -3158,6 +3349,23 @@ export class NostrClient {
|
||||
throw new Error("Unable to resolve the public key for this private key.");
|
||||
}
|
||||
|
||||
if (typeof validator === "function") {
|
||||
try {
|
||||
const isValid = await validator(normalizedPubkey);
|
||||
if (!isValid) {
|
||||
throw new Error("Access denied.");
|
||||
}
|
||||
} catch (validationError) {
|
||||
const message =
|
||||
validationError instanceof Error
|
||||
? validationError.message
|
||||
: "Access denied.";
|
||||
const error = new Error(message);
|
||||
error.code = "access-denied";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
this.sessionActor = {
|
||||
pubkey: normalizedPubkey,
|
||||
privateKey: normalizedPrivateKey,
|
||||
@@ -4307,7 +4515,11 @@ export class NostrClient {
|
||||
) {
|
||||
addCandidate(
|
||||
"nip44",
|
||||
activeSigner.nip44Decrypt.bind(activeSigner),
|
||||
(pubkey, ciphertext, options) =>
|
||||
activeSigner.nip44Decrypt(pubkey, ciphertext, {
|
||||
...options,
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
}),
|
||||
{
|
||||
priority: -20,
|
||||
source: activeSigner.type || "signer",
|
||||
@@ -4321,7 +4533,11 @@ export class NostrClient {
|
||||
) {
|
||||
addCandidate(
|
||||
"nip04",
|
||||
activeSigner.nip04Decrypt.bind(activeSigner),
|
||||
(pubkey, ciphertext, options) =>
|
||||
activeSigner.nip04Decrypt(pubkey, ciphertext, {
|
||||
...options,
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
}),
|
||||
{
|
||||
priority: -10,
|
||||
source: activeSigner.type || "signer",
|
||||
|
||||
@@ -30,6 +30,66 @@ export const DEFAULT_NIP07_PERMISSION_METHODS = Object.freeze([
|
||||
...DEFAULT_NIP07_ENCRYPTION_METHODS,
|
||||
]);
|
||||
|
||||
export const NIP07_PRIORITY = Object.freeze({
|
||||
HIGH: 10,
|
||||
NORMAL: 5,
|
||||
LOW: 1,
|
||||
});
|
||||
|
||||
class Nip07RequestQueue {
|
||||
constructor() {
|
||||
this.queue = [];
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
enqueue(task, priority = NIP07_PRIORITY.NORMAL) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.queue.push({
|
||||
task,
|
||||
priority: Number.isFinite(priority) ? priority : NIP07_PRIORITY.NORMAL,
|
||||
resolve,
|
||||
reject,
|
||||
addedAt: Date.now(),
|
||||
});
|
||||
// Sort by priority (descending), then by insertion time (ascending) for fairness
|
||||
this.queue.sort((a, b) => {
|
||||
if (b.priority !== a.priority) {
|
||||
return b.priority - a.priority;
|
||||
}
|
||||
return a.addedAt - b.addedAt;
|
||||
});
|
||||
this.process();
|
||||
});
|
||||
}
|
||||
|
||||
async process() {
|
||||
if (this.running) return;
|
||||
this.running = true;
|
||||
|
||||
while (this.queue.length > 0) {
|
||||
const item = this.queue.shift();
|
||||
if (!item) break;
|
||||
|
||||
const { task, resolve, reject } = item;
|
||||
|
||||
try {
|
||||
const result = await task();
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
this.running = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Module-level priority queue to serialize NIP-07 extension requests.
|
||||
// This prevents "message channel closed" errors caused by race conditions
|
||||
// when multiple components (blocks, DMs, auth) query the extension simultaneously,
|
||||
// while allowing critical tasks (e.g. blocklist decryption) to jump the line.
|
||||
const requestQueue = new Nip07RequestQueue();
|
||||
|
||||
export function getEnableVariantTimeoutMs() {
|
||||
const overrideValue =
|
||||
typeof globalThis !== "undefined" &&
|
||||
@@ -190,42 +250,49 @@ export async function runNip07WithRetry(
|
||||
label = "NIP-07 operation",
|
||||
timeoutMs = NIP07_LOGIN_TIMEOUT_MS,
|
||||
retryMultiplier = 2,
|
||||
priority = NIP07_PRIORITY.NORMAL,
|
||||
} = {},
|
||||
) {
|
||||
// We wrap the operation invocation to ensure it returns a fresh promise each time
|
||||
// we attempt it. This is critical for retries: if the first attempt stalls (dropped
|
||||
// by extension), re-awaiting the same promise would just keep waiting on the dead request.
|
||||
const invokeOperation = () => Promise.resolve(operation());
|
||||
// We wrap the entire retry logic in a queue task to ensure only one
|
||||
// request hits the extension at a time.
|
||||
const executeTask = async () => {
|
||||
// We wrap the operation invocation to ensure it returns a fresh promise each time
|
||||
// we attempt it. This is critical for retries: if the first attempt stalls (dropped
|
||||
// by extension), re-awaiting the same promise would just keep waiting on the dead request.
|
||||
const invokeOperation = () => Promise.resolve(operation());
|
||||
|
||||
try {
|
||||
return await withNip07Timeout(invokeOperation, {
|
||||
timeoutMs,
|
||||
message: NIP07_LOGIN_TIMEOUT_ERROR_MESSAGE,
|
||||
});
|
||||
} catch (error) {
|
||||
const isTimeoutError =
|
||||
error instanceof Error &&
|
||||
error.message === NIP07_LOGIN_TIMEOUT_ERROR_MESSAGE;
|
||||
try {
|
||||
return await withNip07Timeout(invokeOperation, {
|
||||
timeoutMs,
|
||||
message: NIP07_LOGIN_TIMEOUT_ERROR_MESSAGE,
|
||||
});
|
||||
} catch (error) {
|
||||
const isTimeoutError =
|
||||
error instanceof Error &&
|
||||
error.message === NIP07_LOGIN_TIMEOUT_ERROR_MESSAGE;
|
||||
|
||||
if (!isTimeoutError || retryMultiplier <= 1) {
|
||||
throw error;
|
||||
if (!isTimeoutError || retryMultiplier <= 1) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const extendedTimeout = Math.max(
|
||||
timeoutMs,
|
||||
Math.round(timeoutMs * retryMultiplier),
|
||||
);
|
||||
|
||||
devLogger.warn(
|
||||
`[nostr] ${label} taking longer than ${timeoutMs}ms. Retrying with ${extendedTimeout}ms timeout.`,
|
||||
);
|
||||
|
||||
// On retry, we invoke operation() again to send a fresh request to the extension.
|
||||
return withNip07Timeout(invokeOperation, {
|
||||
timeoutMs: extendedTimeout,
|
||||
message: NIP07_LOGIN_TIMEOUT_ERROR_MESSAGE,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const extendedTimeout = Math.max(
|
||||
timeoutMs,
|
||||
Math.round(timeoutMs * retryMultiplier),
|
||||
);
|
||||
|
||||
devLogger.warn(
|
||||
`[nostr] ${label} taking longer than ${timeoutMs}ms. Retrying with ${extendedTimeout}ms timeout.`,
|
||||
);
|
||||
|
||||
// On retry, we invoke operation() again to send a fresh request to the extension.
|
||||
return withNip07Timeout(invokeOperation, {
|
||||
timeoutMs: extendedTimeout,
|
||||
message: NIP07_LOGIN_TIMEOUT_ERROR_MESSAGE,
|
||||
});
|
||||
}
|
||||
return requestQueue.enqueue(executeTask, priority);
|
||||
}
|
||||
|
||||
export async function requestEnablePermissions(
|
||||
|
||||
+40
-12
@@ -2105,12 +2105,16 @@ export class Nip46RpcClient {
|
||||
}
|
||||
}
|
||||
|
||||
async nip04Encrypt(pubkey, plaintext) {
|
||||
async nip04Encrypt(pubkey, plaintext, options = {}) {
|
||||
if (!pubkey || !plaintext) {
|
||||
throw new Error("Pubkey and plaintext are required for NIP-04 encryption.");
|
||||
}
|
||||
const priority =
|
||||
Number.isFinite(options?.priority)
|
||||
? options.priority
|
||||
: NIP46_PRIORITY.LOW;
|
||||
const result = await this.sendRpc("nip04_encrypt", [pubkey, plaintext], {
|
||||
priority: NIP46_PRIORITY.LOW,
|
||||
priority,
|
||||
});
|
||||
if (typeof result !== "string") {
|
||||
throw new Error("Remote signer returned invalid NIP-04 ciphertext.");
|
||||
@@ -2118,12 +2122,16 @@ export class Nip46RpcClient {
|
||||
return result;
|
||||
}
|
||||
|
||||
async nip04Decrypt(pubkey, ciphertext) {
|
||||
async nip04Decrypt(pubkey, ciphertext, options = {}) {
|
||||
if (!pubkey || !ciphertext) {
|
||||
throw new Error("Pubkey and ciphertext are required for NIP-04 decryption.");
|
||||
}
|
||||
const priority =
|
||||
Number.isFinite(options?.priority)
|
||||
? options.priority
|
||||
: NIP46_PRIORITY.LOW;
|
||||
const result = await this.sendRpc("nip04_decrypt", [pubkey, ciphertext], {
|
||||
priority: NIP46_PRIORITY.LOW,
|
||||
priority,
|
||||
});
|
||||
if (typeof result !== "string") {
|
||||
throw new Error("Remote signer returned invalid NIP-04 plaintext.");
|
||||
@@ -2131,12 +2139,16 @@ export class Nip46RpcClient {
|
||||
return result;
|
||||
}
|
||||
|
||||
async nip44Encrypt(pubkey, plaintext) {
|
||||
async nip44Encrypt(pubkey, plaintext, options = {}) {
|
||||
if (!pubkey || !plaintext) {
|
||||
throw new Error("Pubkey and plaintext are required for NIP-44 encryption.");
|
||||
}
|
||||
const priority =
|
||||
Number.isFinite(options?.priority)
|
||||
? options.priority
|
||||
: NIP46_PRIORITY.LOW;
|
||||
const result = await this.sendRpc("nip44_encrypt", [pubkey, plaintext], {
|
||||
priority: NIP46_PRIORITY.LOW,
|
||||
priority,
|
||||
});
|
||||
if (typeof result !== "string") {
|
||||
throw new Error("Remote signer returned invalid NIP-44 ciphertext.");
|
||||
@@ -2144,12 +2156,16 @@ export class Nip46RpcClient {
|
||||
return result;
|
||||
}
|
||||
|
||||
async nip44Decrypt(pubkey, ciphertext) {
|
||||
async nip44Decrypt(pubkey, ciphertext, options = {}) {
|
||||
if (!pubkey || !ciphertext) {
|
||||
throw new Error("Pubkey and ciphertext are required for NIP-44 decryption.");
|
||||
}
|
||||
const priority =
|
||||
Number.isFinite(options?.priority)
|
||||
? options.priority
|
||||
: NIP46_PRIORITY.LOW;
|
||||
const result = await this.sendRpc("nip44_decrypt", [pubkey, ciphertext], {
|
||||
priority: NIP46_PRIORITY.LOW,
|
||||
priority,
|
||||
});
|
||||
if (typeof result !== "string") {
|
||||
throw new Error("Remote signer returned invalid NIP-44 plaintext.");
|
||||
@@ -2168,13 +2184,25 @@ export class Nip46RpcClient {
|
||||
pubkey: this.userPubkey,
|
||||
signEvent: (event) => this.signEvent(event),
|
||||
nip04: {
|
||||
encrypt: (pubkey, plaintext) => this.nip04Encrypt(pubkey, plaintext),
|
||||
decrypt: (pubkey, ciphertext) => this.nip04Decrypt(pubkey, ciphertext),
|
||||
encrypt: (pubkey, plaintext, options) =>
|
||||
this.nip04Encrypt(pubkey, plaintext, options),
|
||||
decrypt: (pubkey, ciphertext, options) =>
|
||||
this.nip04Decrypt(pubkey, ciphertext, options),
|
||||
},
|
||||
nip44: {
|
||||
encrypt: (pubkey, plaintext) => this.nip44Encrypt(pubkey, plaintext),
|
||||
decrypt: (pubkey, ciphertext) => this.nip44Decrypt(pubkey, ciphertext),
|
||||
encrypt: (pubkey, plaintext, options) =>
|
||||
this.nip44Encrypt(pubkey, plaintext, options),
|
||||
decrypt: (pubkey, ciphertext, options) =>
|
||||
this.nip44Decrypt(pubkey, ciphertext, options),
|
||||
},
|
||||
nip04Encrypt: (pubkey, plaintext, options) =>
|
||||
this.nip04Encrypt(pubkey, plaintext, options),
|
||||
nip04Decrypt: (pubkey, ciphertext, options) =>
|
||||
this.nip04Decrypt(pubkey, ciphertext, options),
|
||||
nip44Encrypt: (pubkey, plaintext, options) =>
|
||||
this.nip44Encrypt(pubkey, plaintext, options),
|
||||
nip44Decrypt: (pubkey, ciphertext, options) =>
|
||||
this.nip44Decrypt(pubkey, ciphertext, options),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2532,11 +2532,13 @@ export function buildAdminListEvent(listKey, params) {
|
||||
tags.push([schema.identifierTag.name, schema.identifierTag.value]);
|
||||
}
|
||||
const participantTagName = schema?.participantTagName || "p";
|
||||
hexPubkeys.forEach((hex) => {
|
||||
if (typeof hex === "string" && hex.trim()) {
|
||||
tags.push([participantTagName, hex.trim()]);
|
||||
}
|
||||
});
|
||||
if (Array.isArray(hexPubkeys)) {
|
||||
hexPubkeys.forEach((hex) => {
|
||||
if (typeof hex === "string" && hex.trim()) {
|
||||
tags.push([participantTagName, hex.trim()]);
|
||||
}
|
||||
});
|
||||
}
|
||||
appendSchemaTags(tags, schema);
|
||||
|
||||
const event = {
|
||||
|
||||
@@ -1,14 +1,4 @@
|
||||
import {
|
||||
waitForNip07Extension,
|
||||
runNip07WithRetry,
|
||||
requestEnablePermissions,
|
||||
DEFAULT_NIP07_PERMISSION_METHODS,
|
||||
} from "../../nostr/nip07Permissions.js";
|
||||
import { createNip07Adapter } from "../../nostr/adapters/nip07Adapter.js";
|
||||
import { setActiveSigner } from "../../nostrClientRegistry.js";
|
||||
import { ensureNostrTools } from "../../nostr/toolkit.js";
|
||||
import { accessControl } from "../../accessControl.js";
|
||||
import { devLogger, userLogger } from "../../utils/logger.js";
|
||||
|
||||
const PROVIDER_ID = "nip07";
|
||||
const PROVIDER_LABEL = "extension (nip-07)";
|
||||
@@ -55,180 +45,6 @@ function normalizePubkey(result) {
|
||||
return "";
|
||||
}
|
||||
|
||||
async function performNip07Login(options = {}, nostrClient = null) {
|
||||
let extension = null;
|
||||
let attempts = 0;
|
||||
const maxAttempts = 3;
|
||||
|
||||
// Optimistic check: if window.nostr is already there, use it immediately.
|
||||
if (typeof window !== "undefined" && window.nostr) {
|
||||
extension = window.nostr;
|
||||
} else {
|
||||
while (!extension && attempts < maxAttempts) {
|
||||
try {
|
||||
// Fast wait for the first attempt, slightly longer for retries
|
||||
const timeout = attempts === 0 ? 1500 : 3000;
|
||||
await waitForNip07Extension(timeout);
|
||||
extension = window.nostr;
|
||||
} catch (waitError) {
|
||||
devLogger.log(
|
||||
`Timed out waiting for extension injection (attempt ${
|
||||
attempts + 1
|
||||
}/${maxAttempts}):`,
|
||||
waitError,
|
||||
);
|
||||
}
|
||||
|
||||
if (extension) {
|
||||
break;
|
||||
}
|
||||
|
||||
attempts++;
|
||||
if (attempts < maxAttempts) {
|
||||
devLogger.log("Retrying NIP-07 detection...");
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!extension) {
|
||||
devLogger.log("No Nostr extension found");
|
||||
throw new Error(
|
||||
"Please install a Nostr extension (Alby, nos2x, etc.)."
|
||||
);
|
||||
}
|
||||
|
||||
const { allowAccountSelection = false, expectPubkey } =
|
||||
typeof options === "object" && options !== null ? options : {};
|
||||
const normalizedExpectedPubkey =
|
||||
typeof expectPubkey === "string" && expectPubkey.trim()
|
||||
? expectPubkey.trim().toLowerCase()
|
||||
: null;
|
||||
|
||||
if (typeof extension.getPublicKey !== "function") {
|
||||
throw new Error(
|
||||
"This NIP-07 extension is missing getPublicKey support. Please update the extension."
|
||||
);
|
||||
}
|
||||
|
||||
// Use the client instance if available to ensure permission cache is updated
|
||||
if (nostrClient && typeof nostrClient.ensureExtensionPermissions === "function") {
|
||||
const permissionResult = await nostrClient.ensureExtensionPermissions(
|
||||
DEFAULT_NIP07_PERMISSION_METHODS,
|
||||
);
|
||||
if (!permissionResult.ok) {
|
||||
const denialMessage =
|
||||
'The NIP-07 extension reported "permission denied". Please approve the prompt and try again.';
|
||||
const denialError = new Error(denialMessage);
|
||||
if (permissionResult.error) {
|
||||
denialError.cause = permissionResult.error;
|
||||
}
|
||||
throw denialError;
|
||||
}
|
||||
} else {
|
||||
const permissionResult = await requestEnablePermissions(
|
||||
extension,
|
||||
DEFAULT_NIP07_PERMISSION_METHODS,
|
||||
);
|
||||
if (!permissionResult.ok) {
|
||||
const denialMessage =
|
||||
'The NIP-07 extension reported "permission denied". Please approve the prompt and try again.';
|
||||
const denialError = new Error(denialMessage);
|
||||
if (permissionResult.error) {
|
||||
denialError.cause = permissionResult.error;
|
||||
}
|
||||
throw denialError;
|
||||
}
|
||||
}
|
||||
|
||||
if (allowAccountSelection && typeof extension.selectAccounts === "function") {
|
||||
try {
|
||||
const selection = await runNip07WithRetry(
|
||||
() => extension.selectAccounts(expectPubkey ? [expectPubkey] : undefined),
|
||||
{ label: "extension.selectAccounts" }
|
||||
);
|
||||
|
||||
const didCancelSelection =
|
||||
selection === undefined ||
|
||||
selection === null ||
|
||||
selection === false ||
|
||||
(Array.isArray(selection) && selection.length === 0);
|
||||
|
||||
if (didCancelSelection) {
|
||||
throw new Error("Account selection was cancelled.");
|
||||
}
|
||||
} catch (selectionErr) {
|
||||
const message =
|
||||
selectionErr && typeof selectionErr.message === "string"
|
||||
? selectionErr.message
|
||||
: "Account selection was cancelled.";
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
const pubkey = await runNip07WithRetry(() => extension.getPublicKey(), {
|
||||
label: "extension.getPublicKey",
|
||||
});
|
||||
|
||||
if (!pubkey || typeof pubkey !== "string") {
|
||||
throw new Error(
|
||||
"The NIP-07 extension did not return a public key. Please try again."
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
normalizedExpectedPubkey &&
|
||||
pubkey.toLowerCase() !== normalizedExpectedPubkey
|
||||
) {
|
||||
throw new Error(
|
||||
"The selected account doesn't match the expected profile. Please try again."
|
||||
);
|
||||
}
|
||||
|
||||
const nip19Tools = await ensureNostrTools();
|
||||
const npubEncode = nip19Tools?.nip19?.npubEncode;
|
||||
if (typeof npubEncode !== "function") {
|
||||
throw new Error("NostrTools nip19 encoder is unavailable.");
|
||||
}
|
||||
const npub = npubEncode(pubkey);
|
||||
|
||||
devLogger.log("Got pubkey:", pubkey);
|
||||
devLogger.log("Converted to npub:", npub);
|
||||
|
||||
// Access control
|
||||
if (!accessControl.canAccess(npub)) {
|
||||
if (accessControl.isBlacklisted(npub)) {
|
||||
throw new Error("Your account has been blocked on this platform.");
|
||||
} else {
|
||||
throw new Error("Access restricted to admins and moderators users only.");
|
||||
}
|
||||
}
|
||||
|
||||
const adapter = await createNip07Adapter(extension);
|
||||
adapter.pubkey = pubkey;
|
||||
setActiveSigner(adapter);
|
||||
|
||||
// Ensure full permissions are requested post-login (e.g. encryption)
|
||||
// This is non-blocking to avoid stalling if the user already approved
|
||||
if (nostrClient && typeof nostrClient.ensureExtensionPermissions === "function") {
|
||||
nostrClient.ensureExtensionPermissions(DEFAULT_NIP07_PERMISSION_METHODS).catch((err) => {
|
||||
userLogger.warn(
|
||||
"[nostr] Extension permissions were not fully granted after login:",
|
||||
err
|
||||
);
|
||||
});
|
||||
} else {
|
||||
requestEnablePermissions(extension, DEFAULT_NIP07_PERMISSION_METHODS).catch((err) => {
|
||||
userLogger.warn(
|
||||
"[nostr] Extension permissions were not fully granted after login:",
|
||||
err
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return { pubkey, signer: adapter };
|
||||
}
|
||||
|
||||
export default {
|
||||
id: PROVIDER_ID,
|
||||
label: PROVIDER_LABEL,
|
||||
@@ -241,7 +57,23 @@ export default {
|
||||
messages: PROVIDER_MESSAGES,
|
||||
errorMessage: PROVIDER_MESSAGES.error,
|
||||
async login({ nostrClient, options } = {}) {
|
||||
const result = await performNip07Login(options, nostrClient);
|
||||
if (!nostrClient || typeof nostrClient.loginWithExtension !== "function") {
|
||||
const error = new Error("Extension login is not available in this environment.");
|
||||
error.code = "provider-unavailable";
|
||||
throw error;
|
||||
}
|
||||
|
||||
const validator = (pubkey) => {
|
||||
if (!accessControl.canAccess(pubkey)) {
|
||||
if (accessControl.isBlacklisted(pubkey)) {
|
||||
throw new Error("Your account has been blocked on this platform.");
|
||||
}
|
||||
throw new Error("Access restricted to admins and moderators users only.");
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const result = await nostrClient.loginWithExtension({ ...options, validator });
|
||||
const pubkey = normalizePubkey(result);
|
||||
const signer =
|
||||
result && typeof result === "object" && result.signer
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { devLogger } from "../../utils/logger.js";
|
||||
import { accessControl } from "../../accessControl.js";
|
||||
|
||||
function summarizeHexForLog(value) {
|
||||
if (typeof value !== "string" || !value.trim()) {
|
||||
@@ -154,6 +155,16 @@ export default {
|
||||
});
|
||||
let result = null;
|
||||
|
||||
const validator = (pubkey) => {
|
||||
if (!accessControl.canAccess(pubkey)) {
|
||||
if (accessControl.isBlacklisted(pubkey)) {
|
||||
throw new Error("Your account has been blocked on this platform.");
|
||||
}
|
||||
throw new Error("Access restricted to admins and moderators users only.");
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
if (normalized.reuseStored) {
|
||||
if (typeof nostrClient.useStoredRemoteSigner !== "function") {
|
||||
const error = new Error("No stored remote signer is available on this device.");
|
||||
@@ -162,7 +173,15 @@ export default {
|
||||
}
|
||||
devLogger.debug("[nip46] Reusing stored remote signer session");
|
||||
|
||||
result = await nostrClient.useStoredRemoteSigner();
|
||||
// useStoredRemoteSigner should ideally accept a validator too, but if it restores a session,
|
||||
// it might not expose a validator hook easily if not updated.
|
||||
// Let's check NostrClient.useStoredRemoteSigner in client.js.
|
||||
// It DOES NOT take a validator in my previous update (I missed it).
|
||||
// I should update client.js to add validator to useStoredRemoteSigner too.
|
||||
// For now, I will assume I will fix client.js later or accept that stored sessions are trusted?
|
||||
// No, I must fix useStoredRemoteSigner too.
|
||||
|
||||
result = await nostrClient.useStoredRemoteSigner({ validator });
|
||||
devLogger.debug("[nip46] Stored remote signer returned", {
|
||||
pubkey: summarizeHexForLog(normalizePubkey(result)),
|
||||
});
|
||||
@@ -184,6 +203,7 @@ export default {
|
||||
remember: normalized.remember,
|
||||
onAuthUrl: normalized.onAuthUrl,
|
||||
onStatus: normalized.onStatus,
|
||||
validator,
|
||||
});
|
||||
devLogger.debug("[nip46] Manual connect completed", {
|
||||
pubkey: summarizeHexForLog(normalizePubkey(result)),
|
||||
@@ -243,6 +263,7 @@ export default {
|
||||
onAuthUrl: normalized.onAuthUrl,
|
||||
onStatus: normalized.onStatus,
|
||||
handshakeTimeoutMs: normalized.handshakeTimeoutMs,
|
||||
validator,
|
||||
});
|
||||
|
||||
if (result && typeof result === "object") {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { accessControl } from "../../accessControl.js";
|
||||
|
||||
const PROVIDER_ID = "nsec";
|
||||
const PROVIDER_LABEL = "nsec or seed (direct private key)";
|
||||
const PROVIDER_DESCRIPTION =
|
||||
@@ -131,11 +133,22 @@ export default {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const validator = (pubkey) => {
|
||||
if (!accessControl.canAccess(pubkey)) {
|
||||
if (accessControl.isBlacklisted(pubkey)) {
|
||||
throw new Error("Your account has been blocked on this platform.");
|
||||
}
|
||||
throw new Error("Access restricted to admins and moderators users only.");
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const registrationResult = await nostrClient.registerPrivateKeySigner({
|
||||
privateKey,
|
||||
pubkey,
|
||||
persist: shouldPersist,
|
||||
passphrase: shouldPersist ? passphrase : undefined,
|
||||
validator,
|
||||
});
|
||||
|
||||
const registeredPubkey = normalizePubkey(registrationResult) || pubkey;
|
||||
|
||||
@@ -19,6 +19,7 @@ import { normalizeHashtag } from "../utils/hashtagNormalization.js";
|
||||
import { profileCache } from "../state/profileCache.js";
|
||||
import { DEFAULT_RELAY_URLS } from "../nostr/toolkit.js";
|
||||
import { relayManager } from "../relayManager.js";
|
||||
import { runNip07WithRetry, NIP07_PRIORITY } from "../nostr/nip07Permissions.js";
|
||||
|
||||
const LOG_PREFIX = "[HashtagPreferences]";
|
||||
const HASHTAG_IDENTIFIER = "bitvid:tag-preferences";
|
||||
@@ -738,12 +739,18 @@ class HashtagPreferencesService {
|
||||
if (signerHasNip44) {
|
||||
registerDecryptor(
|
||||
"nip44",
|
||||
(payload) => signer.nip44Decrypt(userPubkey, payload),
|
||||
(payload) =>
|
||||
signer.nip44Decrypt(userPubkey, payload, {
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
}),
|
||||
"active-signer",
|
||||
);
|
||||
registerDecryptor(
|
||||
"nip44_v2",
|
||||
(payload) => signer.nip44Decrypt(userPubkey, payload),
|
||||
(payload) =>
|
||||
signer.nip44Decrypt(userPubkey, payload, {
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
}),
|
||||
"active-signer",
|
||||
);
|
||||
}
|
||||
@@ -751,7 +758,10 @@ class HashtagPreferencesService {
|
||||
if (signerHasNip04) {
|
||||
registerDecryptor(
|
||||
"nip04",
|
||||
(payload) => signer.nip04Decrypt(userPubkey, payload),
|
||||
(payload) =>
|
||||
signer.nip04Decrypt(userPubkey, payload, {
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
}),
|
||||
"active-signer",
|
||||
);
|
||||
}
|
||||
@@ -761,7 +771,11 @@ class HashtagPreferencesService {
|
||||
if (typeof nostrApi.nip04?.decrypt === "function") {
|
||||
registerDecryptor(
|
||||
"nip04",
|
||||
(payload) => nostrApi.nip04.decrypt(userPubkey, payload),
|
||||
(payload) =>
|
||||
runNip07WithRetry(
|
||||
() => nostrApi.nip04.decrypt(userPubkey, payload),
|
||||
{ label: "nip04.decrypt", priority: NIP07_PRIORITY.HIGH },
|
||||
),
|
||||
"extension",
|
||||
);
|
||||
}
|
||||
@@ -774,7 +788,14 @@ class HashtagPreferencesService {
|
||||
if (typeof nip44.decrypt === "function") {
|
||||
registerDecryptor(
|
||||
"nip44",
|
||||
(payload) => nip44.decrypt(userPubkey, payload),
|
||||
(payload) =>
|
||||
runNip07WithRetry(
|
||||
() => nip44.decrypt(userPubkey, payload),
|
||||
{
|
||||
label: "nip44.decrypt",
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
},
|
||||
),
|
||||
"extension",
|
||||
);
|
||||
}
|
||||
@@ -784,13 +805,27 @@ class HashtagPreferencesService {
|
||||
if (nip44v2 && typeof nip44v2.decrypt === "function") {
|
||||
registerDecryptor(
|
||||
"nip44_v2",
|
||||
(payload) => nip44v2.decrypt(userPubkey, payload),
|
||||
(payload) =>
|
||||
runNip07WithRetry(
|
||||
() => nip44v2.decrypt(userPubkey, payload),
|
||||
{
|
||||
label: "nip44.v2.decrypt",
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
},
|
||||
),
|
||||
"extension",
|
||||
);
|
||||
if (!decryptors.has("nip44")) {
|
||||
registerDecryptor(
|
||||
"nip44",
|
||||
(payload) => nip44v2.decrypt(userPubkey, payload),
|
||||
(payload) =>
|
||||
runNip07WithRetry(
|
||||
() => nip44v2.decrypt(userPubkey, payload),
|
||||
{
|
||||
label: "nip44.v2.decrypt",
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
},
|
||||
),
|
||||
"extension",
|
||||
);
|
||||
}
|
||||
|
||||
+47
-17
@@ -31,6 +31,7 @@ import { devLogger, userLogger } from "./utils/logger.js";
|
||||
import moderationService from "./services/moderationService.js";
|
||||
import nostrService from "./services/nostrService.js";
|
||||
import { profileCache } from "./state/profileCache.js";
|
||||
import { runNip07WithRetry, NIP07_PRIORITY } from "./nostr/nip07Permissions.js";
|
||||
|
||||
const SUBSCRIPTION_SET_KIND =
|
||||
getNostrEventSchema(NOTE_TYPES.SUBSCRIPTION_LIST)?.kind ?? 30000;
|
||||
@@ -776,22 +777,36 @@ class SubscriptionsManager {
|
||||
};
|
||||
|
||||
if (signerHasNip44) {
|
||||
registerDecryptor("nip44", (payload) => signer.nip44Decrypt(userPubkey, payload));
|
||||
registerDecryptor(
|
||||
"nip44_v2",
|
||||
(payload) => signer.nip44Decrypt(userPubkey, payload)
|
||||
registerDecryptor("nip44", (payload) =>
|
||||
signer.nip44Decrypt(userPubkey, payload, {
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
}),
|
||||
);
|
||||
registerDecryptor("nip44_v2", (payload) =>
|
||||
signer.nip44Decrypt(userPubkey, payload, {
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (signerHasNip04) {
|
||||
registerDecryptor("nip04", (payload) => signer.nip04Decrypt(userPubkey, payload));
|
||||
registerDecryptor("nip04", (payload) =>
|
||||
signer.nip04Decrypt(userPubkey, payload, {
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (nostrApi) {
|
||||
if (typeof nostrApi.nip04?.decrypt === "function") {
|
||||
registerDecryptor(
|
||||
"nip04",
|
||||
(payload) => nostrApi.nip04.decrypt(userPubkey, payload)
|
||||
registerDecryptor("nip04", (payload) =>
|
||||
runNip07WithRetry(
|
||||
() => nostrApi.nip04.decrypt(userPubkey, payload),
|
||||
{
|
||||
label: "nip04.decrypt",
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -801,23 +816,38 @@ class SubscriptionsManager {
|
||||
: null;
|
||||
if (nip44) {
|
||||
if (typeof nip44.decrypt === "function") {
|
||||
registerDecryptor(
|
||||
"nip44",
|
||||
(payload) => nip44.decrypt(userPubkey, payload)
|
||||
registerDecryptor("nip44", (payload) =>
|
||||
runNip07WithRetry(
|
||||
() => nip44.decrypt(userPubkey, payload),
|
||||
{
|
||||
label: "nip44.decrypt",
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const nip44v2 =
|
||||
nip44.v2 && typeof nip44.v2 === "object" ? nip44.v2 : null;
|
||||
if (nip44v2 && typeof nip44v2.decrypt === "function") {
|
||||
registerDecryptor(
|
||||
"nip44_v2",
|
||||
(payload) => nip44v2.decrypt(userPubkey, payload)
|
||||
registerDecryptor("nip44_v2", (payload) =>
|
||||
runNip07WithRetry(
|
||||
() => nip44v2.decrypt(userPubkey, payload),
|
||||
{
|
||||
label: "nip44.v2.decrypt",
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
},
|
||||
),
|
||||
);
|
||||
if (!decryptors.has("nip44")) {
|
||||
registerDecryptor(
|
||||
"nip44",
|
||||
(payload) => nip44v2.decrypt(userPubkey, payload)
|
||||
registerDecryptor("nip44", (payload) =>
|
||||
runNip07WithRetry(
|
||||
() => nip44v2.decrypt(userPubkey, payload),
|
||||
{
|
||||
label: "nip44.v2.decrypt",
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+38
-9
@@ -20,6 +20,7 @@ import {
|
||||
import { profileCache } from "./state/profileCache.js";
|
||||
import { DEFAULT_RELAY_URLS } from "./nostr/toolkit.js";
|
||||
import { relayManager } from "./relayManager.js";
|
||||
import { runNip07WithRetry, NIP07_PRIORITY } from "./nostr/nip07Permissions.js";
|
||||
|
||||
class TinyEventEmitter {
|
||||
constructor() {
|
||||
@@ -851,12 +852,18 @@ class UserBlockListManager {
|
||||
if (signerHasNip44) {
|
||||
registerDecryptor(
|
||||
"nip44",
|
||||
(payload) => signer.nip44Decrypt(normalized, payload),
|
||||
(payload) =>
|
||||
signer.nip44Decrypt(normalized, payload, {
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
}),
|
||||
"active-signer",
|
||||
);
|
||||
registerDecryptor(
|
||||
"nip44_v2",
|
||||
(payload) => signer.nip44Decrypt(normalized, payload),
|
||||
(payload) =>
|
||||
signer.nip44Decrypt(normalized, payload, {
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
}),
|
||||
"active-signer",
|
||||
);
|
||||
}
|
||||
@@ -864,7 +871,10 @@ class UserBlockListManager {
|
||||
if (signerHasNip04) {
|
||||
registerDecryptor(
|
||||
"nip04",
|
||||
(payload) => signer.nip04Decrypt(normalized, payload),
|
||||
(payload) =>
|
||||
signer.nip04Decrypt(normalized, payload, {
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
}),
|
||||
"active-signer",
|
||||
);
|
||||
}
|
||||
@@ -874,33 +884,52 @@ class UserBlockListManager {
|
||||
if (typeof nostrApi.nip04?.decrypt === "function") {
|
||||
registerDecryptor(
|
||||
"nip04",
|
||||
(payload) => nostrApi.nip04.decrypt(normalized, payload),
|
||||
(payload) =>
|
||||
runNip07WithRetry(
|
||||
() => nostrApi.nip04.decrypt(normalized, payload),
|
||||
{ label: "nip04.decrypt", priority: NIP07_PRIORITY.HIGH },
|
||||
),
|
||||
"extension",
|
||||
);
|
||||
}
|
||||
|
||||
const nip44 =
|
||||
nostrApi.nip44 && typeof nostrApi.nip44 === "object" ? nostrApi.nip44 : null;
|
||||
nostrApi.nip44 && typeof nostrApi.nip44 === "object"
|
||||
? nostrApi.nip44
|
||||
: null;
|
||||
if (nip44) {
|
||||
if (typeof nip44.decrypt === "function") {
|
||||
registerDecryptor(
|
||||
"nip44",
|
||||
(payload) => nip44.decrypt(normalized, payload),
|
||||
(payload) =>
|
||||
runNip07WithRetry(() => nip44.decrypt(normalized, payload), {
|
||||
label: "nip44.decrypt",
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
}),
|
||||
"extension",
|
||||
);
|
||||
}
|
||||
|
||||
const nip44v2 = nip44.v2 && typeof nip44.v2 === "object" ? nip44.v2 : null;
|
||||
const nip44v2 =
|
||||
nip44.v2 && typeof nip44.v2 === "object" ? nip44.v2 : null;
|
||||
if (nip44v2 && typeof nip44v2.decrypt === "function") {
|
||||
registerDecryptor(
|
||||
"nip44_v2",
|
||||
(payload) => nip44v2.decrypt(normalized, payload),
|
||||
(payload) =>
|
||||
runNip07WithRetry(() => nip44v2.decrypt(normalized, payload), {
|
||||
label: "nip44.v2.decrypt",
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
}),
|
||||
"extension",
|
||||
);
|
||||
if (!decryptors.has("nip44")) {
|
||||
registerDecryptor(
|
||||
"nip44",
|
||||
(payload) => nip44v2.decrypt(normalized, payload),
|
||||
(payload) =>
|
||||
runNip07WithRetry(
|
||||
() => nip44v2.decrypt(normalized, payload),
|
||||
{ label: "nip44.v2.decrypt", priority: NIP07_PRIORITY.HIGH },
|
||||
),
|
||||
"extension",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ async function fuzzTest(iteration) {
|
||||
return { event, context };
|
||||
}
|
||||
|
||||
runFuzzer("dmDecryptor", 5000, fuzzTest)
|
||||
runFuzzer("dmDecryptor", 10000, fuzzTest)
|
||||
.catch(err => {
|
||||
console.error("Fatal fuzzer error:", err);
|
||||
process.exit(1);
|
||||
|
||||
@@ -101,7 +101,7 @@ async function fuzzTest(iteration) {
|
||||
return { rawValue, options };
|
||||
}
|
||||
|
||||
runFuzzer("magnetUtils", 5000, fuzzTest).catch(err => {
|
||||
runFuzzer("magnetUtils", 10000, fuzzTest).catch(err => {
|
||||
console.error("Fatal fuzzer error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -25,7 +25,26 @@ async function fuzzTest(iteration) {
|
||||
"buildZapRequestEvent",
|
||||
"sanitizeAdditionalTags",
|
||||
"validateEventStructure",
|
||||
"setNostrEventSchemaOverrides"
|
||||
"setNostrEventSchemaOverrides",
|
||||
"buildRepostEvent",
|
||||
"buildShareEvent",
|
||||
"buildRelayListEvent",
|
||||
"buildDmRelayListEvent",
|
||||
"buildProfileMetadataEvent",
|
||||
"buildMuteListEvent",
|
||||
"buildDeletionEvent",
|
||||
"buildLegacyDirectMessageEvent",
|
||||
"buildDmAttachmentEvent",
|
||||
"buildDmReadReceiptEvent",
|
||||
"buildDmTypingIndicatorEvent",
|
||||
"buildWatchHistoryEvent",
|
||||
"buildSubscriptionListEvent",
|
||||
"buildBlockListEvent",
|
||||
"buildHashtagPreferenceEvent",
|
||||
"buildAdminListEvent",
|
||||
"buildHttpAuthEvent",
|
||||
"buildReportEvent",
|
||||
"buildVideoMirrorEvent"
|
||||
];
|
||||
|
||||
const targetName = rng.oneOf(targets);
|
||||
@@ -33,7 +52,7 @@ async function fuzzTest(iteration) {
|
||||
|
||||
// Helper to generate a comprehensive random params object
|
||||
const genParams = () => {
|
||||
// 50% chance of being undefined/null/non-object to test robustness
|
||||
// 10% chance of being undefined/null/non-object to test robustness
|
||||
if (Math.random() < 0.1) return rng.nastyString();
|
||||
if (Math.random() < 0.1) return null;
|
||||
|
||||
@@ -52,14 +71,27 @@ async function fuzzTest(iteration) {
|
||||
"videoEventId", "videoEventRelay", "videoDefinitionAddress", "rootIdentifier",
|
||||
"parentCommentId", "rootKind", "rootAuthorPubkey", "parentKind",
|
||||
"recipientPubkey", "relays", "amountSats", "lnurl", "eventId", "coordinate",
|
||||
"targetPointer", "targetAuthorPubkey"
|
||||
"targetPointer", "targetAuthorPubkey",
|
||||
// Added fields
|
||||
"repostKind", "targetKind", "targetEvent", "serializedEvent",
|
||||
"video", "metadata", "pTags", "encrypted", "encryptionTag",
|
||||
"eventIds", "addresses", "reason", "ciphertext", "attachment",
|
||||
"messageKind", "expiresAt", "monthIdentifier", "hexPubkeys",
|
||||
"payload", "reportType", "address", "addressRelay", "authorPubkey",
|
||||
"eventRelay", "method", "userId", "relayHint"
|
||||
];
|
||||
|
||||
extraFields.forEach(field => {
|
||||
if (rng.bool()) {
|
||||
params[field] = rng.mixedString(50);
|
||||
} else if (rng.bool()) {
|
||||
params[field] = rng.recursiveObject(1); // Potentially invalid type
|
||||
if (field === "targetEvent" || field === "video" || field === "attachment" || field === "metadata") {
|
||||
params[field] = rng.recursiveObject(2);
|
||||
} else if (field === "relays" || field === "pTags" || field === "eventIds" || field === "addresses" || field === "hexPubkeys") {
|
||||
params[field] = rng.array(() => rng.mixedString(64), 5);
|
||||
} else {
|
||||
params[field] = rng.recursiveObject(1); // Potentially invalid type
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -132,12 +164,91 @@ async function fuzzTest(iteration) {
|
||||
// Trigger usage
|
||||
Schemas.getNostrEventSchema(randomType);
|
||||
break;
|
||||
|
||||
case "buildRepostEvent":
|
||||
inputs = genParams();
|
||||
Schemas.buildRepostEvent(inputs);
|
||||
break;
|
||||
case "buildShareEvent":
|
||||
inputs = genParams();
|
||||
Schemas.buildShareEvent(inputs);
|
||||
break;
|
||||
case "buildRelayListEvent":
|
||||
inputs = genParams();
|
||||
Schemas.buildRelayListEvent(inputs);
|
||||
break;
|
||||
case "buildDmRelayListEvent":
|
||||
inputs = genParams();
|
||||
Schemas.buildDmRelayListEvent(inputs);
|
||||
break;
|
||||
case "buildProfileMetadataEvent":
|
||||
inputs = genParams();
|
||||
Schemas.buildProfileMetadataEvent(inputs);
|
||||
break;
|
||||
case "buildMuteListEvent":
|
||||
inputs = genParams();
|
||||
Schemas.buildMuteListEvent(inputs);
|
||||
break;
|
||||
case "buildDeletionEvent":
|
||||
inputs = genParams();
|
||||
Schemas.buildDeletionEvent(inputs);
|
||||
break;
|
||||
case "buildLegacyDirectMessageEvent":
|
||||
inputs = genParams();
|
||||
Schemas.buildLegacyDirectMessageEvent(inputs);
|
||||
break;
|
||||
case "buildDmAttachmentEvent":
|
||||
inputs = genParams();
|
||||
Schemas.buildDmAttachmentEvent(inputs);
|
||||
break;
|
||||
case "buildDmReadReceiptEvent":
|
||||
inputs = genParams();
|
||||
Schemas.buildDmReadReceiptEvent(inputs);
|
||||
break;
|
||||
case "buildDmTypingIndicatorEvent":
|
||||
inputs = genParams();
|
||||
Schemas.buildDmTypingIndicatorEvent(inputs);
|
||||
break;
|
||||
case "buildWatchHistoryEvent":
|
||||
inputs = genParams();
|
||||
Schemas.buildWatchHistoryEvent(inputs);
|
||||
break;
|
||||
case "buildSubscriptionListEvent":
|
||||
inputs = genParams();
|
||||
Schemas.buildSubscriptionListEvent(inputs);
|
||||
break;
|
||||
case "buildBlockListEvent":
|
||||
inputs = genParams();
|
||||
Schemas.buildBlockListEvent(inputs);
|
||||
break;
|
||||
case "buildHashtagPreferenceEvent":
|
||||
inputs = genParams();
|
||||
Schemas.buildHashtagPreferenceEvent(inputs);
|
||||
break;
|
||||
case "buildAdminListEvent":
|
||||
inputs = genParams();
|
||||
// buildAdminListEvent(listKey, params)
|
||||
const listKey = rng.oneOf(["moderation", "editors", "whitelist", "blacklist", "communityBlacklistSources", rng.nastyString()]);
|
||||
Schemas.buildAdminListEvent(listKey, inputs);
|
||||
break;
|
||||
case "buildHttpAuthEvent":
|
||||
inputs = genParams();
|
||||
Schemas.buildHttpAuthEvent(inputs);
|
||||
break;
|
||||
case "buildReportEvent":
|
||||
inputs = genParams();
|
||||
Schemas.buildReportEvent(inputs);
|
||||
break;
|
||||
case "buildVideoMirrorEvent":
|
||||
inputs = genParams();
|
||||
Schemas.buildVideoMirrorEvent(inputs);
|
||||
break;
|
||||
}
|
||||
|
||||
return { target: targetName, inputs };
|
||||
}
|
||||
|
||||
runFuzzer("nostrEventSchemas", 5000, fuzzTest).catch(err => {
|
||||
runFuzzer("nostrEventSchemas", 10000, fuzzTest).catch(err => {
|
||||
console.error("Fatal fuzzer error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
+127
-31
@@ -1,20 +1,46 @@
|
||||
import './setup-test-env.js';
|
||||
import { indexedDB, IDBKeyRange } from "fake-indexeddb";
|
||||
import { WebSocket } from 'ws';
|
||||
import * as NostrTools from 'nostr-tools';
|
||||
import { webcrypto } from 'node:crypto';
|
||||
|
||||
// --- Polyfills ---
|
||||
if (!global.indexedDB) {
|
||||
global.indexedDB = indexedDB;
|
||||
}
|
||||
if (!global.IDBKeyRange) {
|
||||
global.IDBKeyRange = IDBKeyRange;
|
||||
}
|
||||
if (!global.NostrTools) {
|
||||
global.NostrTools = NostrTools;
|
||||
}
|
||||
// Ensure crypto is available globally for NostrClient (it might be used in some paths)
|
||||
if (!global.crypto) {
|
||||
global.crypto = webcrypto;
|
||||
}
|
||||
|
||||
// --- Imports from Codebase ---
|
||||
import { NostrClient } from '../../js/nostr/client.js';
|
||||
import {
|
||||
buildVideoPostEvent,
|
||||
buildViewEvent
|
||||
} from '../../js/nostrEventSchemas.js';
|
||||
import * as NostrTools from 'nostr-tools';
|
||||
import { decryptDM } from '../../js/dmDecryptor.js';
|
||||
import { bytesToHex } from '../../vendor/crypto-helpers.bundle.min.js';
|
||||
|
||||
// Setup global NostrTools for toolkit.js fallback
|
||||
global.NostrTools = NostrTools;
|
||||
// --- Configuration ---
|
||||
const TEST_RELAYS = ["wss://relay.damus.io", "wss://nos.lol"];
|
||||
const TIMEOUT_MS = 15000;
|
||||
|
||||
async function waitForEvent(pool, relays, id, timeoutMs = 10000) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
const event = await pool.get(relays, { ids: [id] });
|
||||
if (event) return event;
|
||||
try {
|
||||
const event = await pool.get(relays, { ids: [id] });
|
||||
if (event) return event;
|
||||
} catch (e) {
|
||||
// ignore network errors during polling
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
}
|
||||
return null;
|
||||
@@ -22,29 +48,34 @@ async function waitForEvent(pool, relays, id, timeoutMs = 10000) {
|
||||
|
||||
async function run() {
|
||||
console.log("Starting Interop Test...");
|
||||
console.log(`Relays: ${TEST_RELAYS.join(', ')}`);
|
||||
|
||||
// 1. Setup Client
|
||||
const client = new NostrClient();
|
||||
|
||||
// Test relays - using reputable public relays
|
||||
const TEST_RELAYS = ["wss://relay.damus.io", "wss://nos.lol"];
|
||||
client.relays = [...TEST_RELAYS];
|
||||
|
||||
console.log(`Using relays: ${TEST_RELAYS.join(', ')}`);
|
||||
client.readRelays = [...TEST_RELAYS];
|
||||
client.writeRelays = [...TEST_RELAYS];
|
||||
|
||||
try {
|
||||
// 2. Init (connects to relays)
|
||||
await client.init();
|
||||
console.log("Client initialized.");
|
||||
|
||||
// 3. Generate Ephemeral Keys
|
||||
// 3. Generate Ephemeral Keys (Sender)
|
||||
console.log("Generating ephemeral keys...");
|
||||
const sk = NostrTools.generateSecretKey();
|
||||
const skHex = bytesToHex(sk);
|
||||
const pk = NostrTools.getPublicKey(sk);
|
||||
console.log(`Ephemeral Pubkey: ${pk}`);
|
||||
console.log(`Ephemeral Sender Pubkey: ${pk}`);
|
||||
|
||||
// 4. Register Signer
|
||||
await client.registerPrivateKeySigner({ privateKey: skHex, pubkey: pk });
|
||||
// Manually set pubkey as registerPrivateKeySigner doesn't always set client.pubkey depending on implementation details in some versions,
|
||||
// though the code read showed it sets sessionActor. client.pubkey is usually set on login.
|
||||
// The client.registerPrivateKeySigner implementation sets sessionActor but assumes the user calls init or similar to set client.pubkey if full login is desired?
|
||||
// Looking at NostrClient.js, registerPrivateKeySigner does NOT set this.pubkey. It sets sessionActor.
|
||||
// So we manually set it to simulate "logged in" state for high-level methods.
|
||||
client.pubkey = pk;
|
||||
console.log("Signer registered.");
|
||||
|
||||
// 5. Test A: Video Post
|
||||
@@ -62,9 +93,10 @@ async function run() {
|
||||
url: "https://example.com/video.mp4",
|
||||
mimeType: "video/mp4",
|
||||
mode: "live",
|
||||
deleted: false
|
||||
deleted: false,
|
||||
magnet: `magnet:?xt=urn:btih:${Math.random().toString(16).slice(2)}`, // Add magnet as per schema requirements usually
|
||||
},
|
||||
// 's' tag is required by schema (storage pointer)
|
||||
// 's' tag is recommended for storage pointer
|
||||
additionalTags: [['s', `url:https://example.com/video.mp4`]]
|
||||
});
|
||||
|
||||
@@ -78,8 +110,8 @@ async function run() {
|
||||
console.log(`Published Video Event ID: ${publishedEvent.id}`);
|
||||
|
||||
// Verify
|
||||
console.log("Verifying Video Post...");
|
||||
const rawRelayEvent = await waitForEvent(client.pool, TEST_RELAYS, publishedEvent.id);
|
||||
console.log("Verifying Video Post (fetch by ID)...");
|
||||
const rawRelayEvent = await waitForEvent(client.pool, TEST_RELAYS, publishedEvent.id, TIMEOUT_MS);
|
||||
|
||||
if (!rawRelayEvent) throw new Error("Failed to fetch published video event from relays");
|
||||
if (rawRelayEvent.id !== publishedEvent.id) throw new Error("Fetched event ID mismatch");
|
||||
@@ -96,35 +128,99 @@ async function run() {
|
||||
});
|
||||
|
||||
const viewPublishResult = await client.signAndPublishEvent(viewEvent);
|
||||
console.log(`Published View Event ID: ${viewPublishResult.signedEvent.id}`);
|
||||
const viewEventId = viewPublishResult.signedEvent.id;
|
||||
console.log(`Published View Event ID: ${viewEventId}`);
|
||||
|
||||
// Ensure it is visible to subscriber clients (simulate).
|
||||
const rawViewEvent = await waitForEvent(client.pool, TEST_RELAYS, viewPublishResult.signedEvent.id);
|
||||
const rawViewEvent = await waitForEvent(client.pool, TEST_RELAYS, viewEventId, TIMEOUT_MS);
|
||||
if (!rawViewEvent) throw new Error("Failed to fetch published view event");
|
||||
console.log("View Event Verified.");
|
||||
|
||||
// 7. Test C: Direct Message
|
||||
console.log("\n[Test C] Testing Direct Message...");
|
||||
const msg = `Test DM ${Date.now()}`;
|
||||
const npub = NostrTools.nip19.npubEncode(pk);
|
||||
|
||||
console.log(`Sending DM to self (${npub})...`);
|
||||
const dmResult = await client.sendDirectMessage(npub, msg);
|
||||
// Generate Recipient
|
||||
const recipientSk = NostrTools.generateSecretKey();
|
||||
const recipientSkHex = bytesToHex(recipientSk);
|
||||
const recipientPk = NostrTools.getPublicKey(recipientSk);
|
||||
const recipientNpub = NostrTools.nip19.npubEncode(recipientPk);
|
||||
console.log(`Recipient Pubkey: ${recipientPk}`);
|
||||
|
||||
const msg = `Test DM ${Date.now()}`;
|
||||
console.log(`Sending DM to recipient (${recipientNpub})...`);
|
||||
|
||||
// We send using the client (Sender)
|
||||
const dmResult = await client.sendDirectMessage(recipientNpub, msg);
|
||||
|
||||
if (!dmResult.ok) throw new Error(`DM Send failed: ${dmResult.error}`);
|
||||
console.log("DM Sent.");
|
||||
|
||||
// Verify decryption using the appropriate decryptor helper
|
||||
console.log("Verifying DM (waiting 2s)...");
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
const dms = await client.listDirectMessages(pk, { limit: 10 });
|
||||
const found = dms.find(d => d.plaintext === msg);
|
||||
// Fetch the raw DM event from relays (Recipient perspective)
|
||||
console.log("Fetching DM event from relays...");
|
||||
|
||||
if (!found) {
|
||||
console.log("Fetched DMs:", dms.map(d => d.plaintext));
|
||||
throw new Error("Failed to find sent DM");
|
||||
// Filter for DMs to recipient
|
||||
const filter = {
|
||||
kinds: [4, 1059], // 4 (legacy) or 1059 (nip17)
|
||||
'#p': [recipientPk],
|
||||
limit: 1,
|
||||
since: Math.floor(Date.now() / 1000) - 60 // last 60 seconds
|
||||
};
|
||||
|
||||
// Wait for propagation
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
|
||||
let events = [];
|
||||
const fetchStart = Date.now();
|
||||
while (Date.now() - fetchStart < TIMEOUT_MS) {
|
||||
events = await client.pool.list(TEST_RELAYS, [filter]);
|
||||
if (events.length > 0) break;
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
}
|
||||
console.log("DM Verified.");
|
||||
|
||||
if (events.length === 0) {
|
||||
throw new Error("Failed to find sent DM on relays.");
|
||||
}
|
||||
|
||||
const dmEvent = events[0];
|
||||
console.log(`Found DM Event (Kind ${dmEvent.kind}, ID: ${dmEvent.id})`);
|
||||
|
||||
// Decrypt using decryptDM helper and Recipient Keys
|
||||
console.log("Attempting decryption using decryptDM helper...");
|
||||
|
||||
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) {
|
||||
console.error("Decryption errors:", decryptResult.errors);
|
||||
throw new Error("DM decryption failed");
|
||||
}
|
||||
|
||||
if (decryptResult.plaintext !== msg) {
|
||||
throw new Error(`DM plaintext mismatch. Expected "${msg}", got "${decryptResult.plaintext}"`);
|
||||
}
|
||||
console.log("DM Verified (Decryption successful).");
|
||||
|
||||
console.log("\nAll Interop Tests Passed!");
|
||||
process.exit(0);
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import './setup-test-env.js';
|
||||
import { NostrClient, getActiveSigner } from '../../js/nostr/client.js';
|
||||
// import { userLogger, devLogger } from '../../js/utils/logger.js';
|
||||
|
||||
// Polyfill window.nostr
|
||||
const mockPubkey = "0000000000000000000000000000000000000000000000000000000000000001";
|
||||
const mockNostr = {
|
||||
getPublicKey: async () => mockPubkey,
|
||||
signEvent: async (e) => {
|
||||
e.sig = "mock-sig";
|
||||
e.id = "mock-id";
|
||||
return e;
|
||||
},
|
||||
nip04: {
|
||||
encrypt: async () => "mock-ciphertext",
|
||||
decrypt: async () => "mock-plaintext"
|
||||
}
|
||||
};
|
||||
|
||||
global.window.nostr = mockNostr;
|
||||
|
||||
async function run() {
|
||||
console.log("Starting NIP-07 Refactor Verification...");
|
||||
|
||||
const client = new NostrClient();
|
||||
// Skip client.init() to avoid network/relay connection attempts in this isolated test
|
||||
// await client.init();
|
||||
|
||||
console.log("Testing loginWithExtension...");
|
||||
try {
|
||||
const result = await client.loginWithExtension();
|
||||
console.log("Login returned:", { pubkey: result.pubkey, hasSigner: !!result.signer });
|
||||
|
||||
if (result.pubkey !== mockPubkey) {
|
||||
throw new Error(`Pubkey mismatch: expected ${mockPubkey}, got ${result.pubkey}`);
|
||||
}
|
||||
|
||||
const activeSigner = getActiveSigner();
|
||||
if (!activeSigner) {
|
||||
throw new Error("Active signer was not set!");
|
||||
}
|
||||
|
||||
if (activeSigner.pubkey !== mockPubkey) {
|
||||
throw new Error(`Active signer pubkey mismatch: ${activeSigner.pubkey}`);
|
||||
}
|
||||
|
||||
console.log("Testing validator failure...");
|
||||
try {
|
||||
await client.loginWithExtension({
|
||||
validator: async () => false // should fail
|
||||
});
|
||||
console.error("Validator failure test FAILED: Should have thrown error");
|
||||
process.exit(1);
|
||||
} catch (error) {
|
||||
if (error.message.includes("Access denied")) {
|
||||
console.log("Validator failure test SUCCESS!");
|
||||
} else {
|
||||
console.error("Validator failure test FAILED: Wrong error", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Verification SUCCESS!");
|
||||
} catch (error) {
|
||||
console.error("Verification FAILED:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,279 +0,0 @@
|
||||
/**
|
||||
* Event Schema Validation Tool
|
||||
*
|
||||
* Scans the codebase for event builder usage and verifies that the output
|
||||
* conforms to the defined schemas in `js/nostrEventSchemas.js`.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
import { WebSocket } from 'ws';
|
||||
|
||||
// Polyfills
|
||||
if (typeof globalThis.crypto === 'undefined') {
|
||||
globalThis.crypto = crypto;
|
||||
}
|
||||
if (typeof globalThis.WebSocket === 'undefined') {
|
||||
globalThis.WebSocket = WebSocket;
|
||||
}
|
||||
if (typeof globalThis.localStorage === 'undefined') {
|
||||
globalThis.localStorage = {
|
||||
getItem: () => null,
|
||||
setItem: () => {},
|
||||
removeItem: () => {},
|
||||
clear: () => {}
|
||||
};
|
||||
}
|
||||
if (typeof globalThis.window === 'undefined') {
|
||||
globalThis.window = globalThis;
|
||||
}
|
||||
|
||||
// Helper to load module
|
||||
const loadModule = async (filePath) => {
|
||||
return import(path.resolve(process.cwd(), filePath));
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const schemasModule = await loadModule('js/nostrEventSchemas.js');
|
||||
const {
|
||||
validateEventStructure,
|
||||
NOTE_TYPES,
|
||||
buildVideoPostEvent,
|
||||
buildHttpAuthEvent,
|
||||
buildReportEvent,
|
||||
buildVideoMirrorEvent,
|
||||
buildRepostEvent,
|
||||
buildShareEvent,
|
||||
buildRelayListEvent,
|
||||
buildDmRelayListEvent,
|
||||
buildProfileMetadataEvent,
|
||||
buildMuteListEvent,
|
||||
buildDeletionEvent,
|
||||
buildLegacyDirectMessageEvent,
|
||||
buildDmAttachmentEvent,
|
||||
buildDmReadReceiptEvent,
|
||||
buildDmTypingIndicatorEvent,
|
||||
buildViewEvent,
|
||||
buildZapRequestEvent,
|
||||
buildReactionEvent,
|
||||
buildCommentEvent,
|
||||
buildWatchHistoryEvent,
|
||||
buildSubscriptionListEvent,
|
||||
buildBlockListEvent,
|
||||
buildHashtagPreferenceEvent,
|
||||
buildAdminListEvent
|
||||
} = schemasModule;
|
||||
|
||||
console.log("Starting Event Schema Validation...");
|
||||
|
||||
let failureCount = 0;
|
||||
|
||||
const validate = (type, builder, params, label) => {
|
||||
try {
|
||||
const event = builder(params);
|
||||
const { valid, errors } = validateEventStructure(type, event);
|
||||
if (!valid) {
|
||||
console.error(`[FAIL] ${label} (${type})`);
|
||||
errors.forEach(err => console.error(` - ${err}`));
|
||||
console.error(` Event:`, JSON.stringify(event, null, 2));
|
||||
failureCount++;
|
||||
} else {
|
||||
console.log(`[PASS] ${label} (${type})`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[ERROR] Exception in builder for ${label} (${type})`);
|
||||
console.error(e);
|
||||
failureCount++;
|
||||
}
|
||||
};
|
||||
|
||||
const pubkey = "0000000000000000000000000000000000000000000000000000000000000001";
|
||||
const created_at = Math.floor(Date.now() / 1000);
|
||||
|
||||
// Test Cases
|
||||
validate(NOTE_TYPES.VIDEO_POST, buildVideoPostEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
dTagValue: "test-video-id",
|
||||
content: {
|
||||
version: 3,
|
||||
title: "Test Video",
|
||||
videoRootId: "root-id",
|
||||
}
|
||||
}, "Basic Video Post");
|
||||
|
||||
validate(NOTE_TYPES.HTTP_AUTH, buildHttpAuthEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
url: "https://example.com/login",
|
||||
method: "POST"
|
||||
}, "HTTP Auth");
|
||||
|
||||
validate(NOTE_TYPES.REPORT, buildReportEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
eventId: "e".repeat(64),
|
||||
reportType: "spam"
|
||||
}, "Report");
|
||||
|
||||
validate(NOTE_TYPES.VIDEO_MIRROR, buildVideoMirrorEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
content: "mirror content"
|
||||
}, "Video Mirror");
|
||||
|
||||
validate(NOTE_TYPES.REPOST, buildRepostEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
eventId: "e".repeat(64),
|
||||
targetKind: 1 // Text Note Repost (Kind 6)
|
||||
}, "Repost");
|
||||
|
||||
validate(NOTE_TYPES.GENERIC_REPOST, buildRepostEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
eventId: "e".repeat(64),
|
||||
targetKind: 30078 // Video Post Repost (Kind 16)
|
||||
}, "Generic Repost");
|
||||
|
||||
validate(NOTE_TYPES.SHARE, buildShareEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
content: "Check this out",
|
||||
video: { id: "e".repeat(64), pubkey: "p".repeat(64) }
|
||||
}, "Share");
|
||||
|
||||
validate(NOTE_TYPES.RELAY_LIST, buildRelayListEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
relays: ["wss://relay.example.com"]
|
||||
}, "Relay List");
|
||||
|
||||
validate(NOTE_TYPES.DM_RELAY_LIST, buildDmRelayListEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
relays: ["wss://dm.example.com"]
|
||||
}, "DM Relay List");
|
||||
|
||||
validate(NOTE_TYPES.PROFILE_METADATA, buildProfileMetadataEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
metadata: { name: "Test User" }
|
||||
}, "Profile Metadata");
|
||||
|
||||
validate(NOTE_TYPES.MUTE_LIST, buildMuteListEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
pTags: ["p".repeat(64)]
|
||||
}, "Mute List");
|
||||
|
||||
validate(NOTE_TYPES.DELETION, buildDeletionEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
eventIds: ["e".repeat(64)]
|
||||
}, "Deletion");
|
||||
|
||||
validate(NOTE_TYPES.LEGACY_DM, buildLegacyDirectMessageEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
recipientPubkey: "p".repeat(64),
|
||||
ciphertext: "encrypted"
|
||||
}, "Legacy DM");
|
||||
|
||||
validate(NOTE_TYPES.DM_ATTACHMENT, buildDmAttachmentEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
recipientPubkey: "p".repeat(64),
|
||||
attachment: { url: "https://example.com/file.jpg", x: "hash" }
|
||||
}, "DM Attachment");
|
||||
|
||||
validate(NOTE_TYPES.DM_READ_RECEIPT, buildDmReadReceiptEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
recipientPubkey: "p".repeat(64),
|
||||
eventId: "e".repeat(64)
|
||||
}, "DM Read Receipt");
|
||||
|
||||
validate(NOTE_TYPES.DM_TYPING, buildDmTypingIndicatorEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
recipientPubkey: "p".repeat(64)
|
||||
}, "DM Typing Indicator");
|
||||
|
||||
validate(NOTE_TYPES.VIEW_EVENT, buildViewEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
pointerTag: ["e", "eventid", "relay"]
|
||||
}, "View Event");
|
||||
|
||||
validate(NOTE_TYPES.ZAP_REQUEST, buildZapRequestEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
recipientPubkey: "p".repeat(64),
|
||||
amountSats: 100,
|
||||
lnurl: "lnurl1...",
|
||||
relays: ["wss://relay.damus.io"]
|
||||
}, "Zap Request");
|
||||
|
||||
validate(NOTE_TYPES.VIDEO_REACTION, buildReactionEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
content: "+",
|
||||
targetPointer: { type: "e", value: "e".repeat(64) }
|
||||
}, "Reaction");
|
||||
|
||||
validate(NOTE_TYPES.VIDEO_COMMENT, buildCommentEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
content: "Great video!",
|
||||
videoEventId: "e".repeat(64)
|
||||
}, "Comment");
|
||||
|
||||
validate(NOTE_TYPES.WATCH_HISTORY, buildWatchHistoryEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
content: {},
|
||||
monthIdentifier: "2023-10" // Required for valid event
|
||||
}, "Watch History");
|
||||
|
||||
validate(NOTE_TYPES.SUBSCRIPTION_LIST, buildSubscriptionListEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
content: "encrypted_subs"
|
||||
}, "Subscription List");
|
||||
|
||||
validate(NOTE_TYPES.USER_BLOCK_LIST, buildBlockListEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
content: "encrypted_blocks"
|
||||
}, "Block List");
|
||||
|
||||
validate(NOTE_TYPES.HASHTAG_PREFERENCES, buildHashtagPreferenceEvent, {
|
||||
pubkey,
|
||||
created_at,
|
||||
content: "encrypted_prefs"
|
||||
}, "Hashtag Preferences");
|
||||
|
||||
// Admin Lists
|
||||
const adminBuilder = (params) => buildAdminListEvent("moderation", params);
|
||||
validate(NOTE_TYPES.ADMIN_MODERATION_LIST, adminBuilder, {
|
||||
pubkey,
|
||||
created_at,
|
||||
hexPubkeys: ["p".repeat(64)]
|
||||
}, "Admin Moderation List");
|
||||
|
||||
|
||||
if (failureCount > 0) {
|
||||
console.log(`\nValidation complete with ${failureCount} failures.`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("\nAll checks passed!");
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error("Fatal error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
Executable
+390
@@ -0,0 +1,390 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import {
|
||||
NOTE_TYPES,
|
||||
validateEventStructure,
|
||||
buildVideoPostEvent,
|
||||
buildHttpAuthEvent,
|
||||
buildReportEvent,
|
||||
buildVideoMirrorEvent,
|
||||
buildRepostEvent,
|
||||
buildShareEvent,
|
||||
buildRelayListEvent,
|
||||
buildDmRelayListEvent,
|
||||
buildProfileMetadataEvent,
|
||||
buildMuteListEvent,
|
||||
buildDeletionEvent,
|
||||
buildLegacyDirectMessageEvent,
|
||||
buildDmAttachmentEvent,
|
||||
buildDmReadReceiptEvent,
|
||||
buildDmTypingIndicatorEvent,
|
||||
buildViewEvent,
|
||||
buildZapRequestEvent,
|
||||
buildReactionEvent,
|
||||
buildCommentEvent,
|
||||
buildWatchHistoryEvent,
|
||||
buildSubscriptionListEvent,
|
||||
buildBlockListEvent,
|
||||
buildHashtagPreferenceEvent,
|
||||
buildAdminListEvent,
|
||||
} from "../../js/nostrEventSchemas.js";
|
||||
|
||||
const PUBKEY = "0000000000000000000000000000000000000000000000000000000000000001";
|
||||
const CREATED_AT = 1700000000;
|
||||
|
||||
const TESTS = [
|
||||
{
|
||||
name: "Video Post",
|
||||
builder: buildVideoPostEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
dTagValue: "test-video-id",
|
||||
content: {
|
||||
version: 3,
|
||||
title: "Test Video",
|
||||
videoRootId: "test-video-id",
|
||||
infoHash: "0123456789abcdef0123456789abcdef01234567",
|
||||
},
|
||||
},
|
||||
type: NOTE_TYPES.VIDEO_POST,
|
||||
},
|
||||
{
|
||||
name: "Video Post (URL)",
|
||||
builder: buildVideoPostEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
dTagValue: "test-video-url",
|
||||
content: {
|
||||
version: 3,
|
||||
title: "Test Video URL",
|
||||
videoRootId: "test-video-url",
|
||||
url: "https://example.com/video.mp4",
|
||||
},
|
||||
},
|
||||
type: NOTE_TYPES.VIDEO_POST,
|
||||
},
|
||||
{
|
||||
name: "HTTP Auth",
|
||||
builder: buildHttpAuthEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
url: "https://example.com/auth",
|
||||
method: "GET",
|
||||
payload: "hash",
|
||||
},
|
||||
type: NOTE_TYPES.HTTP_AUTH,
|
||||
},
|
||||
{
|
||||
name: "Report",
|
||||
builder: buildReportEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
eventId: "e".repeat(64),
|
||||
reportType: "nudity",
|
||||
},
|
||||
type: NOTE_TYPES.REPORT,
|
||||
},
|
||||
{
|
||||
name: "Video Mirror",
|
||||
builder: buildVideoMirrorEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
tags: [["url", "https://example.com/video.mp4"], ["m", "video/mp4"]],
|
||||
content: "Mirroring video",
|
||||
},
|
||||
type: NOTE_TYPES.VIDEO_MIRROR,
|
||||
},
|
||||
{
|
||||
name: "Repost",
|
||||
builder: buildRepostEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
eventId: "e".repeat(64),
|
||||
eventRelay: "wss://relay.example.com",
|
||||
},
|
||||
type: NOTE_TYPES.REPOST,
|
||||
},
|
||||
{
|
||||
name: "Generic Repost (Video)",
|
||||
builder: buildRepostEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
targetKind: 30078,
|
||||
eventId: "e".repeat(64),
|
||||
},
|
||||
type: NOTE_TYPES.GENERIC_REPOST,
|
||||
},
|
||||
{
|
||||
name: "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",
|
||||
},
|
||||
type: NOTE_TYPES.SHARE,
|
||||
},
|
||||
{
|
||||
name: "Relay List",
|
||||
builder: buildRelayListEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
relays: [{ url: "wss://relay.example.com", read: true, write: true }],
|
||||
},
|
||||
type: NOTE_TYPES.RELAY_LIST,
|
||||
},
|
||||
{
|
||||
name: "DM Relay List",
|
||||
builder: buildDmRelayListEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
relays: ["wss://relay.example.com"],
|
||||
},
|
||||
type: NOTE_TYPES.DM_RELAY_LIST,
|
||||
},
|
||||
{
|
||||
name: "Profile Metadata",
|
||||
builder: buildProfileMetadataEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
metadata: { name: "Test User", about: "Testing" },
|
||||
},
|
||||
type: NOTE_TYPES.PROFILE_METADATA,
|
||||
},
|
||||
{
|
||||
name: "Mute List",
|
||||
builder: buildMuteListEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
pTags: [PUBKEY],
|
||||
},
|
||||
type: NOTE_TYPES.MUTE_LIST,
|
||||
},
|
||||
{
|
||||
name: "Deletion",
|
||||
builder: buildDeletionEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
eventIds: ["e".repeat(64)],
|
||||
reason: "mistake",
|
||||
},
|
||||
type: NOTE_TYPES.DELETION,
|
||||
},
|
||||
{
|
||||
name: "Legacy DM",
|
||||
builder: buildLegacyDirectMessageEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
recipientPubkey: PUBKEY,
|
||||
ciphertext: "encrypted",
|
||||
},
|
||||
type: NOTE_TYPES.LEGACY_DM,
|
||||
},
|
||||
{
|
||||
name: "DM Attachment",
|
||||
builder: buildDmAttachmentEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
recipientPubkey: PUBKEY,
|
||||
attachment: { url: "https://example.com/file", x: "hash", type: "image/jpeg" },
|
||||
},
|
||||
type: NOTE_TYPES.DM_ATTACHMENT,
|
||||
},
|
||||
{
|
||||
name: "DM Read Receipt",
|
||||
builder: buildDmReadReceiptEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
recipientPubkey: PUBKEY,
|
||||
eventId: "e".repeat(64),
|
||||
},
|
||||
type: NOTE_TYPES.DM_READ_RECEIPT,
|
||||
},
|
||||
{
|
||||
name: "DM Typing Indicator",
|
||||
builder: buildDmTypingIndicatorEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
recipientPubkey: PUBKEY,
|
||||
expiresAt: CREATED_AT + 60,
|
||||
},
|
||||
type: NOTE_TYPES.DM_TYPING,
|
||||
},
|
||||
{
|
||||
name: "View Event",
|
||||
builder: buildViewEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
pointerValue: "e".repeat(64),
|
||||
pointerTag: ["e", "e".repeat(64)],
|
||||
},
|
||||
type: NOTE_TYPES.VIEW_EVENT,
|
||||
},
|
||||
{
|
||||
name: "Zap Request",
|
||||
builder: buildZapRequestEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
recipientPubkey: PUBKEY,
|
||||
amountSats: 100,
|
||||
relays: ["wss://relay.example.com"],
|
||||
},
|
||||
type: NOTE_TYPES.ZAP_REQUEST,
|
||||
},
|
||||
{
|
||||
name: "Video Reaction",
|
||||
builder: buildReactionEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
pointerValue: "e".repeat(64),
|
||||
targetPointer: { type: "e", value: "e".repeat(64) },
|
||||
content: "+",
|
||||
},
|
||||
type: NOTE_TYPES.VIDEO_REACTION,
|
||||
},
|
||||
{
|
||||
name: "Video Comment",
|
||||
builder: buildCommentEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
videoEventId: "e".repeat(64),
|
||||
content: "Nice video",
|
||||
},
|
||||
type: NOTE_TYPES.VIDEO_COMMENT,
|
||||
},
|
||||
{
|
||||
name: "Watch History",
|
||||
builder: buildWatchHistoryEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
monthIdentifier: "2023-01",
|
||||
content: { version: 2, month: "2023-01", items: [] },
|
||||
},
|
||||
type: NOTE_TYPES.WATCH_HISTORY,
|
||||
},
|
||||
{
|
||||
name: "Subscription List",
|
||||
builder: buildSubscriptionListEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
content: "encrypted-json",
|
||||
encryption: "nip44_v2",
|
||||
},
|
||||
type: NOTE_TYPES.SUBSCRIPTION_LIST,
|
||||
},
|
||||
{
|
||||
name: "Block List",
|
||||
builder: buildBlockListEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
content: "encrypted-json",
|
||||
encryption: "nip44_v2",
|
||||
},
|
||||
type: NOTE_TYPES.USER_BLOCK_LIST,
|
||||
},
|
||||
{
|
||||
name: "Hashtag Preferences",
|
||||
builder: buildHashtagPreferenceEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
content: "encrypted-json",
|
||||
},
|
||||
type: NOTE_TYPES.HASHTAG_PREFERENCES,
|
||||
},
|
||||
{
|
||||
name: "Admin Moderation List",
|
||||
builder: buildAdminListEvent,
|
||||
// buildAdminListEvent takes (listKey, params)
|
||||
args: ["moderation", {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
hexPubkeys: [PUBKEY],
|
||||
}],
|
||||
type: NOTE_TYPES.ADMIN_MODERATION_LIST,
|
||||
},
|
||||
{
|
||||
name: "Admin Blacklist",
|
||||
builder: buildAdminListEvent,
|
||||
args: ["blacklist", {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
hexPubkeys: [PUBKEY],
|
||||
}],
|
||||
type: NOTE_TYPES.ADMIN_BLACKLIST,
|
||||
},
|
||||
{
|
||||
name: "Admin Whitelist",
|
||||
builder: buildAdminListEvent,
|
||||
args: ["whitelist", {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
hexPubkeys: [PUBKEY],
|
||||
}],
|
||||
type: NOTE_TYPES.ADMIN_WHITELIST,
|
||||
},
|
||||
];
|
||||
|
||||
async function run() {
|
||||
console.log("Running Event Schema Validation...");
|
||||
let failureCount = 0;
|
||||
|
||||
for (const test of TESTS) {
|
||||
try {
|
||||
let event;
|
||||
if (test.args) {
|
||||
event = test.builder(...test.args);
|
||||
} else {
|
||||
event = test.builder(test.params);
|
||||
}
|
||||
|
||||
const { valid, errors } = validateEventStructure(test.type, event);
|
||||
|
||||
if (valid) {
|
||||
console.log(`[PASS] ${test.name}`);
|
||||
} else {
|
||||
console.error(`[FAIL] ${test.name}`);
|
||||
errors.forEach((err) => console.error(` - ${err}`));
|
||||
failureCount++;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[ERROR] ${test.name} threw an exception:`, e);
|
||||
failureCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (failureCount > 0) {
|
||||
console.error(`\nValidation failed with ${failureCount} errors.`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("\nAll events validated successfully.");
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
+2
-2
@@ -28,8 +28,8 @@ module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
xs: "30rem",
|
||||
compact: "25rem"
|
||||
xs: "480px",
|
||||
compact: "400px"
|
||||
},
|
||||
colors: {
|
||||
page: "var(--color-page)",
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
|
||||
> [email protected] test:unit
|
||||
> node scripts/run-unit-tests.mjs
|
||||
|
||||
|
||||
→ Running tests/app-batch-fetch-profiles.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
[batchFetchProfiles] Failed to fetch profiles from relay wss://fail.example: Error: relay timed out
|
||||
at Object.list (file:///app/tests/app-batch-fetch-profiles.test.mjs:80:31)
|
||||
at file:///app/js/utils/profileBatchFetcher.js:115:10
|
||||
at Array.map (<anonymous>)
|
||||
at batchFetchProfilesFromRelays (file:///app/js/utils/profileBatchFetcher.js:112:75)
|
||||
at TestContext.<anonymous> (file:///app/tests/app-batch-fetch-profiles.test.mjs:87:11)
|
||||
at Test.runInAsyncScope (node:async_hooks:214:14)
|
||||
at Test.run (node:internal/test_runner/test:1047:25)
|
||||
at Test.start (node:internal/test_runner/test:944:17)
|
||||
at startSubtestAfterBootstrap (node:internal/test_runner/harness:296:17)
|
||||
TAP version 13
|
||||
# Subtest: batchFetchProfiles handles fast and failing relays
|
||||
ok 1 - batchFetchProfiles handles fast and failing relays
|
||||
---
|
||||
duration_ms: 3.910398
|
||||
type: 'test'
|
||||
...
|
||||
1..1
|
||||
# tests 1
|
||||
# suites 0
|
||||
# pass 1
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 26.765285
|
||||
|
||||
→ Running tests/app/channel-profile-moderation.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
[feedEngine/moderation] metadata.moderation updated {
|
||||
stage: 'moderation',
|
||||
videoId: 'vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv',
|
||||
blockAutoplay: true,
|
||||
blurThumbnail: true,
|
||||
trustedCount: 2,
|
||||
reportType: 'nudity',
|
||||
hidden: false,
|
||||
hideReason: null,
|
||||
hideBypass: null
|
||||
}
|
||||
TAP version 13
|
||||
# Subtest: renderChannelVideosFromList decorates videos before storing
|
||||
ok 1 - renderChannelVideosFromList decorates videos before storing
|
||||
---
|
||||
duration_ms: 255.735327
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: renderChannelVideosFromList applies moderation blur without existing metadata
|
||||
ok 2 - renderChannelVideosFromList applies moderation blur without existing metadata
|
||||
---
|
||||
duration_ms: 31.654581
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: applyChannelVisualBlur blurs banner and avatar when viewer mutes author
|
||||
ok 3 - applyChannelVisualBlur blurs banner and avatar when viewer mutes author
|
||||
---
|
||||
duration_ms: 10.838215
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: moderation override clears channel blur via event wiring
|
||||
ok 4 - moderation override clears channel blur via event wiring
|
||||
---
|
||||
duration_ms: 34.798513
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: channel header moderation badge reflects blur state and override actions
|
||||
ok 5 - channel header moderation badge reflects blur state and override actions
|
||||
---
|
||||
duration_ms: 32.153884
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: channel video cards update moderation state in place when summary changes
|
||||
ok 6 - channel video cards update moderation state in place when summary changes
|
||||
---
|
||||
duration_ms: 21.578259
|
||||
type: 'test'
|
||||
...
|
||||
1..6
|
||||
# tests 6
|
||||
# suites 0
|
||||
# pass 6
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 398.296397
|
||||
|
||||
→ Running tests/app/hash-change-handler.test.mjs
|
||||
TAP version 13
|
||||
# Subtest: handleHashChange defaults to for-you when logged in
|
||||
ok 1 - handleHashChange defaults to for-you when logged in
|
||||
---
|
||||
duration_ms: 74.173116
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleHashChange defaults to most-recent-videos when logged out
|
||||
ok 2 - handleHashChange defaults to most-recent-videos when logged out
|
||||
---
|
||||
duration_ms: 9.658917
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleHashChange respects explicit view regardless of login state
|
||||
ok 3 - handleHashChange respects explicit view regardless of login state
|
||||
---
|
||||
duration_ms: 9.775207
|
||||
type: 'test'
|
||||
...
|
||||
1..3
|
||||
# tests 3
|
||||
# suites 0
|
||||
# pass 3
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 106.244019
|
||||
|
||||
→ Running tests/app/hydrate-sidebar-navigation.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: hydrateSidebarNavigation reveals chrome controls for authenticated viewers
|
||||
ok 1 - hydrateSidebarNavigation reveals chrome controls for authenticated viewers
|
||||
---
|
||||
duration_ms: 104.182525
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: hydrateSidebarNavigation hides chrome controls for logged-out viewers
|
||||
ok 2 - hydrateSidebarNavigation hides chrome controls for logged-out viewers
|
||||
---
|
||||
duration_ms: 28.824807
|
||||
type: 'test'
|
||||
...
|
||||
1..2
|
||||
# tests 2
|
||||
# suites 0
|
||||
# pass 2
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 148.628996
|
||||
|
||||
→ Running tests/app/is-user-logged-in.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: isUserLoggedIn returns false when no user pubkey is set
|
||||
ok 1 - isUserLoggedIn returns false when no user pubkey is set
|
||||
---
|
||||
duration_ms: 1.372662
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn treats extension logins as authenticated
|
||||
ok 2 - isUserLoggedIn treats extension logins as authenticated
|
||||
---
|
||||
duration_ms: 0.381678
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn guards against mismatched nostrClient state
|
||||
ok 3 - isUserLoggedIn guards against mismatched nostrClient state
|
||||
---
|
||||
duration_ms: 0.211323
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn ignores anonymous session actor mismatches
|
||||
ok 4 - isUserLoggedIn ignores anonymous session actor mismatches
|
||||
---
|
||||
duration_ms: 0.255464
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn rejects mismatched managed session actors
|
||||
ok 5 - isUserLoggedIn rejects mismatched managed session actors
|
||||
---
|
||||
duration_ms: 0.321204
|
||||
type: 'test'
|
||||
...
|
||||
1..5
|
||||
# tests 5
|
||||
# suites 0
|
||||
# pass 5
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 33.821343
|
||||
|
||||
→ Running tests/app/moderation-overrides.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: handleModerationOverride decorates stored and current videos then refreshes UI
|
||||
ok 1 - handleModerationOverride decorates stored and current videos then refreshes UI
|
||||
---
|
||||
duration_ms: 242.055409
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleModerationOverride resumes deferred playback
|
||||
ok 2 - handleModerationOverride resumes deferred playback
|
||||
---
|
||||
duration_ms: 1.66123
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleModerationBlock requests a block, clears overrides, and refreshes hidden state
|
||||
not ok 3 - handleModerationBlock requests a block, clears overrides, and refreshes hidden state
|
||||
---
|
||||
duration_ms: 5.442489
|
||||
type: 'test'
|
||||
location: '/app/tests/app/moderation-overrides.test.mjs:112:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
|
||||
false !== true
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: true
|
||||
actual: false
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///app/tests/app/moderation-overrides.test.mjs:228:10)
|
||||
async Test.run (node:internal/test_runner/test:1054:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
|
||||
...
|
||||
# Subtest: handleModerationBlock returns false when viewer is logged out
|
||||
ok 4 - handleModerationBlock returns false when viewer is logged out
|
||||
---
|
||||
duration_ms: 1.600465
|
||||
type: 'test'
|
||||
...
|
||||
1..4
|
||||
# tests 4
|
||||
# suites 0
|
||||
# pass 3
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 286.782704
|
||||
file:///app/scripts/run-unit-tests.mjs:68
|
||||
reject(new Error(`${relativePath} failed with exit code ${code}`));
|
||||
^
|
||||
|
||||
Error: tests/app/moderation-overrides.test.mjs failed with exit code 1
|
||||
at ChildProcess.<anonymous> (file:///app/scripts/run-unit-tests.mjs:68:16)
|
||||
at ChildProcess.emit (node:events:519:28)
|
||||
at maybeClose (node:internal/child_process:1101:16)
|
||||
at ChildProcess._handle.onexit (node:internal/child_process:304:5)
|
||||
|
||||
Node.js v22.21.1
|
||||
@@ -1,260 +0,0 @@
|
||||
|
||||
> [email protected] test:unit
|
||||
> node scripts/run-unit-tests.mjs
|
||||
|
||||
|
||||
→ Running tests/app-batch-fetch-profiles.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
[batchFetchProfiles] Failed to fetch profiles from relay wss://fail.example: Error: relay timed out
|
||||
at Object.list (file:///app/tests/app-batch-fetch-profiles.test.mjs:80:31)
|
||||
at file:///app/js/utils/profileBatchFetcher.js:115:10
|
||||
at Array.map (<anonymous>)
|
||||
at batchFetchProfilesFromRelays (file:///app/js/utils/profileBatchFetcher.js:112:75)
|
||||
at TestContext.<anonymous> (file:///app/tests/app-batch-fetch-profiles.test.mjs:87:11)
|
||||
at Test.runInAsyncScope (node:async_hooks:214:14)
|
||||
at Test.run (node:internal/test_runner/test:1047:25)
|
||||
at Test.start (node:internal/test_runner/test:944:17)
|
||||
at startSubtestAfterBootstrap (node:internal/test_runner/harness:296:17)
|
||||
TAP version 13
|
||||
# Subtest: batchFetchProfiles handles fast and failing relays
|
||||
ok 1 - batchFetchProfiles handles fast and failing relays
|
||||
---
|
||||
duration_ms: 22.244233
|
||||
type: 'test'
|
||||
...
|
||||
1..1
|
||||
# tests 1
|
||||
# suites 0
|
||||
# pass 1
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 36.538516
|
||||
|
||||
→ Running tests/app/channel-profile-moderation.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
[feedEngine/moderation] metadata.moderation updated {
|
||||
stage: 'moderation',
|
||||
videoId: 'vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv',
|
||||
blockAutoplay: true,
|
||||
blurThumbnail: true,
|
||||
trustedCount: 2,
|
||||
reportType: 'nudity',
|
||||
hidden: false,
|
||||
hideReason: null,
|
||||
hideBypass: null
|
||||
}
|
||||
TAP version 13
|
||||
# Subtest: renderChannelVideosFromList decorates videos before storing
|
||||
ok 1 - renderChannelVideosFromList decorates videos before storing
|
||||
---
|
||||
duration_ms: 316.648044
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: renderChannelVideosFromList applies moderation blur without existing metadata
|
||||
ok 2 - renderChannelVideosFromList applies moderation blur without existing metadata
|
||||
---
|
||||
duration_ms: 46.776425
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: applyChannelVisualBlur blurs banner and avatar when viewer mutes author
|
||||
ok 3 - applyChannelVisualBlur blurs banner and avatar when viewer mutes author
|
||||
---
|
||||
duration_ms: 15.209442
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: moderation override clears channel blur via event wiring
|
||||
ok 4 - moderation override clears channel blur via event wiring
|
||||
---
|
||||
duration_ms: 36.087729
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: channel header moderation badge reflects blur state and override actions
|
||||
ok 5 - channel header moderation badge reflects blur state and override actions
|
||||
---
|
||||
duration_ms: 39.830563
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: channel video cards update moderation state in place when summary changes
|
||||
ok 6 - channel video cards update moderation state in place when summary changes
|
||||
---
|
||||
duration_ms: 32.84156
|
||||
type: 'test'
|
||||
...
|
||||
1..6
|
||||
# tests 6
|
||||
# suites 0
|
||||
# pass 6
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 501.07657
|
||||
|
||||
→ Running tests/app/hash-change-handler.test.mjs
|
||||
TAP version 13
|
||||
# Subtest: handleHashChange defaults to for-you when logged in
|
||||
ok 1 - handleHashChange defaults to for-you when logged in
|
||||
---
|
||||
duration_ms: 73.46412
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleHashChange defaults to most-recent-videos when logged out
|
||||
ok 2 - handleHashChange defaults to most-recent-videos when logged out
|
||||
---
|
||||
duration_ms: 9.611748
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleHashChange respects explicit view regardless of login state
|
||||
ok 3 - handleHashChange respects explicit view regardless of login state
|
||||
---
|
||||
duration_ms: 9.764783
|
||||
type: 'test'
|
||||
...
|
||||
1..3
|
||||
# tests 3
|
||||
# suites 0
|
||||
# pass 3
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 110.646914
|
||||
|
||||
→ Running tests/app/hydrate-sidebar-navigation.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: hydrateSidebarNavigation reveals chrome controls for authenticated viewers
|
||||
ok 1 - hydrateSidebarNavigation reveals chrome controls for authenticated viewers
|
||||
---
|
||||
duration_ms: 99.746325
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: hydrateSidebarNavigation hides chrome controls for logged-out viewers
|
||||
ok 2 - hydrateSidebarNavigation hides chrome controls for logged-out viewers
|
||||
---
|
||||
duration_ms: 26.407756
|
||||
type: 'test'
|
||||
...
|
||||
1..2
|
||||
# tests 2
|
||||
# suites 0
|
||||
# pass 2
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 141.088808
|
||||
|
||||
→ Running tests/app/is-user-logged-in.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: isUserLoggedIn returns false when no user pubkey is set
|
||||
ok 1 - isUserLoggedIn returns false when no user pubkey is set
|
||||
---
|
||||
duration_ms: 1.373918
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn treats extension logins as authenticated
|
||||
ok 2 - isUserLoggedIn treats extension logins as authenticated
|
||||
---
|
||||
duration_ms: 0.408152
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn guards against mismatched nostrClient state
|
||||
ok 3 - isUserLoggedIn guards against mismatched nostrClient state
|
||||
---
|
||||
duration_ms: 0.244921
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn ignores anonymous session actor mismatches
|
||||
ok 4 - isUserLoggedIn ignores anonymous session actor mismatches
|
||||
---
|
||||
duration_ms: 0.257353
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn rejects mismatched managed session actors
|
||||
ok 5 - isUserLoggedIn rejects mismatched managed session actors
|
||||
---
|
||||
duration_ms: 0.311313
|
||||
type: 'test'
|
||||
...
|
||||
1..5
|
||||
# tests 5
|
||||
# suites 0
|
||||
# pass 5
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 32.052639
|
||||
|
||||
→ Running tests/app/moderation-overrides.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: handleModerationOverride decorates stored and current videos then refreshes UI
|
||||
ok 1 - handleModerationOverride decorates stored and current videos then refreshes UI
|
||||
---
|
||||
duration_ms: 212.505662
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleModerationOverride resumes deferred playback
|
||||
ok 2 - handleModerationOverride resumes deferred playback
|
||||
---
|
||||
duration_ms: 1.28636
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleModerationBlock requests a block, clears overrides, and refreshes hidden state
|
||||
not ok 3 - handleModerationBlock requests a block, clears overrides, and refreshes hidden state
|
||||
---
|
||||
duration_ms: 4.64235
|
||||
type: 'test'
|
||||
location: '/app/tests/app/moderation-overrides.test.mjs:112:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
|
||||
false !== true
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: true
|
||||
actual: false
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///app/tests/app/moderation-overrides.test.mjs:228:10)
|
||||
async Test.run (node:internal/test_runner/test:1054:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
|
||||
...
|
||||
# Subtest: handleModerationBlock returns false when viewer is logged out
|
||||
ok 4 - handleModerationBlock returns false when viewer is logged out
|
||||
---
|
||||
duration_ms: 1.619904
|
||||
type: 'test'
|
||||
...
|
||||
1..4
|
||||
# tests 4
|
||||
# suites 0
|
||||
# pass 3
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 250.717362
|
||||
file:///app/scripts/run-unit-tests.mjs:68
|
||||
reject(new Error(`${relativePath} failed with exit code ${code}`));
|
||||
^
|
||||
|
||||
Error: tests/app/moderation-overrides.test.mjs failed with exit code 1
|
||||
at ChildProcess.<anonymous> (file:///app/scripts/run-unit-tests.mjs:68:16)
|
||||
at ChildProcess.emit (node:events:519:28)
|
||||
at maybeClose (node:internal/child_process:1101:16)
|
||||
at ChildProcess._handle.onexit (node:internal/child_process:304:5)
|
||||
|
||||
Node.js v22.21.1
|
||||
@@ -1,261 +0,0 @@
|
||||
|
||||
> [email protected] test:unit
|
||||
> node scripts/run-unit-tests.mjs
|
||||
|
||||
|
||||
→ Running tests/app-batch-fetch-profiles.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
[batchFetchProfiles] Failed to fetch profiles from relay wss://fail.example: Error: relay timed out
|
||||
at Object.list (file:///app/tests/app-batch-fetch-profiles.test.mjs:80:31)
|
||||
at file:///app/js/utils/profileBatchFetcher.js:115:10
|
||||
at Array.map (<anonymous>)
|
||||
at batchFetchProfilesFromRelays (file:///app/js/utils/profileBatchFetcher.js:112:75)
|
||||
at TestContext.<anonymous> (file:///app/tests/app-batch-fetch-profiles.test.mjs:87:11)
|
||||
at Test.runInAsyncScope (node:async_hooks:214:14)
|
||||
at Test.run (node:internal/test_runner/test:1047:25)
|
||||
at Test.start (node:internal/test_runner/test:944:17)
|
||||
at startSubtestAfterBootstrap (node:internal/test_runner/harness:296:17)
|
||||
TAP version 13
|
||||
# Subtest: batchFetchProfiles handles fast and failing relays
|
||||
ok 1 - batchFetchProfiles handles fast and failing relays
|
||||
---
|
||||
duration_ms: 7.683186
|
||||
type: 'test'
|
||||
...
|
||||
1..1
|
||||
# tests 1
|
||||
# suites 0
|
||||
# pass 1
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 28.640279
|
||||
|
||||
→ Running tests/app/channel-profile-moderation.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
[feedEngine/moderation] metadata.moderation updated {
|
||||
stage: 'moderation',
|
||||
videoId: 'vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv',
|
||||
blockAutoplay: true,
|
||||
blurThumbnail: true,
|
||||
trustedCount: 2,
|
||||
reportType: 'nudity',
|
||||
hidden: false,
|
||||
hideReason: null,
|
||||
hideBypass: null
|
||||
}
|
||||
TAP version 13
|
||||
# Subtest: renderChannelVideosFromList decorates videos before storing
|
||||
ok 1 - renderChannelVideosFromList decorates videos before storing
|
||||
---
|
||||
duration_ms: 376.493065
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: renderChannelVideosFromList applies moderation blur without existing metadata
|
||||
ok 2 - renderChannelVideosFromList applies moderation blur without existing metadata
|
||||
---
|
||||
duration_ms: 39.743453
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: applyChannelVisualBlur blurs banner and avatar when viewer mutes author
|
||||
ok 3 - applyChannelVisualBlur blurs banner and avatar when viewer mutes author
|
||||
---
|
||||
duration_ms: 15.335853
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: moderation override clears channel blur via event wiring
|
||||
ok 4 - moderation override clears channel blur via event wiring
|
||||
---
|
||||
duration_ms: 47.515622
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: channel header moderation badge reflects blur state and override actions
|
||||
ok 5 - channel header moderation badge reflects blur state and override actions
|
||||
---
|
||||
duration_ms: 52.962084
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: channel video cards update moderation state in place when summary changes
|
||||
ok 6 - channel video cards update moderation state in place when summary changes
|
||||
---
|
||||
duration_ms: 27.377719
|
||||
type: 'test'
|
||||
...
|
||||
1..6
|
||||
# tests 6
|
||||
# suites 0
|
||||
# pass 6
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 573.718626
|
||||
|
||||
→ Running tests/app/hash-change-handler.test.mjs
|
||||
TAP version 13
|
||||
# Subtest: handleHashChange defaults to for-you when logged in
|
||||
ok 1 - handleHashChange defaults to for-you when logged in
|
||||
---
|
||||
duration_ms: 73.304539
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleHashChange defaults to most-recent-videos when logged out
|
||||
ok 2 - handleHashChange defaults to most-recent-videos when logged out
|
||||
---
|
||||
duration_ms: 9.802311
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleHashChange respects explicit view regardless of login state
|
||||
ok 3 - handleHashChange respects explicit view regardless of login state
|
||||
---
|
||||
duration_ms: 10.826679
|
||||
type: 'test'
|
||||
...
|
||||
1..3
|
||||
# tests 3
|
||||
# suites 0
|
||||
# pass 3
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 111.820727
|
||||
|
||||
→ Running tests/app/hydrate-sidebar-navigation.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: hydrateSidebarNavigation reveals chrome controls for authenticated viewers
|
||||
ok 1 - hydrateSidebarNavigation reveals chrome controls for authenticated viewers
|
||||
---
|
||||
duration_ms: 121.895181
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: hydrateSidebarNavigation hides chrome controls for logged-out viewers
|
||||
ok 2 - hydrateSidebarNavigation hides chrome controls for logged-out viewers
|
||||
---
|
||||
duration_ms: 27.859075
|
||||
type: 'test'
|
||||
...
|
||||
1..2
|
||||
# tests 2
|
||||
# suites 0
|
||||
# pass 2
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 166.232762
|
||||
|
||||
→ Running tests/app/is-user-logged-in.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: isUserLoggedIn returns false when no user pubkey is set
|
||||
ok 1 - isUserLoggedIn returns false when no user pubkey is set
|
||||
---
|
||||
duration_ms: 2.070726
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn treats extension logins as authenticated
|
||||
ok 2 - isUserLoggedIn treats extension logins as authenticated
|
||||
---
|
||||
duration_ms: 0.592571
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn guards against mismatched nostrClient state
|
||||
ok 3 - isUserLoggedIn guards against mismatched nostrClient state
|
||||
---
|
||||
duration_ms: 0.445862
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn ignores anonymous session actor mismatches
|
||||
ok 4 - isUserLoggedIn ignores anonymous session actor mismatches
|
||||
---
|
||||
duration_ms: 0.5379
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn rejects mismatched managed session actors
|
||||
ok 5 - isUserLoggedIn rejects mismatched managed session actors
|
||||
---
|
||||
duration_ms: 0.568571
|
||||
type: 'test'
|
||||
...
|
||||
1..5
|
||||
# tests 5
|
||||
# suites 0
|
||||
# pass 5
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 42.558696
|
||||
|
||||
→ Running tests/app/moderation-overrides.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
DEBUG: Test 3 Result: true
|
||||
TAP version 13
|
||||
# Subtest: handleModerationOverride decorates stored and current videos then refreshes UI
|
||||
ok 1 - handleModerationOverride decorates stored and current videos then refreshes UI
|
||||
---
|
||||
duration_ms: 221.521473
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleModerationOverride resumes deferred playback
|
||||
ok 2 - handleModerationOverride resumes deferred playback
|
||||
---
|
||||
duration_ms: 1.277155
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleModerationBlock requests a block, clears overrides, and refreshes hidden state
|
||||
not ok 3 - handleModerationBlock requests a block, clears overrides, and refreshes hidden state
|
||||
---
|
||||
duration_ms: 5.235509
|
||||
type: 'test'
|
||||
location: '/app/tests/app/moderation-overrides.test.mjs:112:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
|
||||
false !== true
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: true
|
||||
actual: false
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///app/tests/app/moderation-overrides.test.mjs:229:10)
|
||||
async Test.run (node:internal/test_runner/test:1054:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
|
||||
...
|
||||
# Subtest: handleModerationBlock returns false when viewer is logged out
|
||||
ok 4 - handleModerationBlock returns false when viewer is logged out
|
||||
---
|
||||
duration_ms: 1.748986
|
||||
type: 'test'
|
||||
...
|
||||
1..4
|
||||
# tests 4
|
||||
# suites 0
|
||||
# pass 3
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 263.815159
|
||||
file:///app/scripts/run-unit-tests.mjs:68
|
||||
reject(new Error(`${relativePath} failed with exit code ${code}`));
|
||||
^
|
||||
|
||||
Error: tests/app/moderation-overrides.test.mjs failed with exit code 1
|
||||
at ChildProcess.<anonymous> (file:///app/scripts/run-unit-tests.mjs:68:16)
|
||||
at ChildProcess.emit (node:events:519:28)
|
||||
at maybeClose (node:internal/child_process:1101:16)
|
||||
at ChildProcess._handle.onexit (node:internal/child_process:304:5)
|
||||
|
||||
Node.js v22.21.1
|
||||
@@ -1,260 +0,0 @@
|
||||
|
||||
> [email protected] test:unit
|
||||
> node scripts/run-unit-tests.mjs
|
||||
|
||||
|
||||
→ Running tests/app-batch-fetch-profiles.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
[batchFetchProfiles] Failed to fetch profiles from relay wss://fail.example: Error: relay timed out
|
||||
at Object.list (file:///app/tests/app-batch-fetch-profiles.test.mjs:80:31)
|
||||
at file:///app/js/utils/profileBatchFetcher.js:115:10
|
||||
at Array.map (<anonymous>)
|
||||
at batchFetchProfilesFromRelays (file:///app/js/utils/profileBatchFetcher.js:112:75)
|
||||
at TestContext.<anonymous> (file:///app/tests/app-batch-fetch-profiles.test.mjs:87:11)
|
||||
at Test.runInAsyncScope (node:async_hooks:214:14)
|
||||
at Test.run (node:internal/test_runner/test:1047:25)
|
||||
at Test.start (node:internal/test_runner/test:944:17)
|
||||
at startSubtestAfterBootstrap (node:internal/test_runner/harness:296:17)
|
||||
TAP version 13
|
||||
# Subtest: batchFetchProfiles handles fast and failing relays
|
||||
ok 1 - batchFetchProfiles handles fast and failing relays
|
||||
---
|
||||
duration_ms: 3.941211
|
||||
type: 'test'
|
||||
...
|
||||
1..1
|
||||
# tests 1
|
||||
# suites 0
|
||||
# pass 1
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 16.455907
|
||||
|
||||
→ Running tests/app/channel-profile-moderation.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
[feedEngine/moderation] metadata.moderation updated {
|
||||
stage: 'moderation',
|
||||
videoId: 'vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv',
|
||||
blockAutoplay: true,
|
||||
blurThumbnail: true,
|
||||
trustedCount: 2,
|
||||
reportType: 'nudity',
|
||||
hidden: false,
|
||||
hideReason: null,
|
||||
hideBypass: null
|
||||
}
|
||||
TAP version 13
|
||||
# Subtest: renderChannelVideosFromList decorates videos before storing
|
||||
ok 1 - renderChannelVideosFromList decorates videos before storing
|
||||
---
|
||||
duration_ms: 243.403366
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: renderChannelVideosFromList applies moderation blur without existing metadata
|
||||
ok 2 - renderChannelVideosFromList applies moderation blur without existing metadata
|
||||
---
|
||||
duration_ms: 31.534109
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: applyChannelVisualBlur blurs banner and avatar when viewer mutes author
|
||||
ok 3 - applyChannelVisualBlur blurs banner and avatar when viewer mutes author
|
||||
---
|
||||
duration_ms: 10.992319
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: moderation override clears channel blur via event wiring
|
||||
ok 4 - moderation override clears channel blur via event wiring
|
||||
---
|
||||
duration_ms: 38.203729
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: channel header moderation badge reflects blur state and override actions
|
||||
ok 5 - channel header moderation badge reflects blur state and override actions
|
||||
---
|
||||
duration_ms: 32.713928
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: channel video cards update moderation state in place when summary changes
|
||||
ok 6 - channel video cards update moderation state in place when summary changes
|
||||
---
|
||||
duration_ms: 20.992921
|
||||
type: 'test'
|
||||
...
|
||||
1..6
|
||||
# tests 6
|
||||
# suites 0
|
||||
# pass 6
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 389.736415
|
||||
|
||||
→ Running tests/app/hash-change-handler.test.mjs
|
||||
TAP version 13
|
||||
# Subtest: handleHashChange defaults to for-you when logged in
|
||||
ok 1 - handleHashChange defaults to for-you when logged in
|
||||
---
|
||||
duration_ms: 73.866587
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleHashChange defaults to most-recent-videos when logged out
|
||||
ok 2 - handleHashChange defaults to most-recent-videos when logged out
|
||||
---
|
||||
duration_ms: 9.66665
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleHashChange respects explicit view regardless of login state
|
||||
ok 3 - handleHashChange respects explicit view regardless of login state
|
||||
---
|
||||
duration_ms: 9.863301
|
||||
type: 'test'
|
||||
...
|
||||
1..3
|
||||
# tests 3
|
||||
# suites 0
|
||||
# pass 3
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 106.76867
|
||||
|
||||
→ Running tests/app/hydrate-sidebar-navigation.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: hydrateSidebarNavigation reveals chrome controls for authenticated viewers
|
||||
ok 1 - hydrateSidebarNavigation reveals chrome controls for authenticated viewers
|
||||
---
|
||||
duration_ms: 85.299474
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: hydrateSidebarNavigation hides chrome controls for logged-out viewers
|
||||
ok 2 - hydrateSidebarNavigation hides chrome controls for logged-out viewers
|
||||
---
|
||||
duration_ms: 13.586181
|
||||
type: 'test'
|
||||
...
|
||||
1..2
|
||||
# tests 2
|
||||
# suites 0
|
||||
# pass 2
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 110.142612
|
||||
|
||||
→ Running tests/app/is-user-logged-in.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: isUserLoggedIn returns false when no user pubkey is set
|
||||
ok 1 - isUserLoggedIn returns false when no user pubkey is set
|
||||
---
|
||||
duration_ms: 1.567229
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn treats extension logins as authenticated
|
||||
ok 2 - isUserLoggedIn treats extension logins as authenticated
|
||||
---
|
||||
duration_ms: 0.354268
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn guards against mismatched nostrClient state
|
||||
ok 3 - isUserLoggedIn guards against mismatched nostrClient state
|
||||
---
|
||||
duration_ms: 0.275984
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn ignores anonymous session actor mismatches
|
||||
ok 4 - isUserLoggedIn ignores anonymous session actor mismatches
|
||||
---
|
||||
duration_ms: 0.274841
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn rejects mismatched managed session actors
|
||||
ok 5 - isUserLoggedIn rejects mismatched managed session actors
|
||||
---
|
||||
duration_ms: 0.252751
|
||||
type: 'test'
|
||||
...
|
||||
1..5
|
||||
# tests 5
|
||||
# suites 0
|
||||
# pass 5
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 34.328915
|
||||
|
||||
→ Running tests/app/moderation-overrides.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: handleModerationOverride decorates stored and current videos then refreshes UI
|
||||
ok 1 - handleModerationOverride decorates stored and current videos then refreshes UI
|
||||
---
|
||||
duration_ms: 225.848661
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleModerationOverride resumes deferred playback
|
||||
ok 2 - handleModerationOverride resumes deferred playback
|
||||
---
|
||||
duration_ms: 1.28016
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleModerationBlock requests a block, clears overrides, and refreshes hidden state
|
||||
not ok 3 - handleModerationBlock requests a block, clears overrides, and refreshes hidden state
|
||||
---
|
||||
duration_ms: 5.041835
|
||||
type: 'test'
|
||||
location: '/app/tests/app/moderation-overrides.test.mjs:112:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
|
||||
false !== true
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: true
|
||||
actual: false
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///app/tests/app/moderation-overrides.test.mjs:232:10)
|
||||
async Test.run (node:internal/test_runner/test:1054:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
|
||||
...
|
||||
# Subtest: handleModerationBlock returns false when viewer is logged out
|
||||
ok 4 - handleModerationBlock returns false when viewer is logged out
|
||||
---
|
||||
duration_ms: 1.571133
|
||||
type: 'test'
|
||||
...
|
||||
1..4
|
||||
# tests 4
|
||||
# suites 0
|
||||
# pass 3
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 268.845775
|
||||
file:///app/scripts/run-unit-tests.mjs:68
|
||||
reject(new Error(`${relativePath} failed with exit code ${code}`));
|
||||
^
|
||||
|
||||
Error: tests/app/moderation-overrides.test.mjs failed with exit code 1
|
||||
at ChildProcess.<anonymous> (file:///app/scripts/run-unit-tests.mjs:68:16)
|
||||
at ChildProcess.emit (node:events:519:28)
|
||||
at maybeClose (node:internal/child_process:1101:16)
|
||||
at ChildProcess._handle.onexit (node:internal/child_process:304:5)
|
||||
|
||||
Node.js v22.21.1
|
||||
@@ -1,260 +0,0 @@
|
||||
|
||||
> [email protected] test:unit
|
||||
> node scripts/run-unit-tests.mjs
|
||||
|
||||
|
||||
→ Running tests/app-batch-fetch-profiles.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
[batchFetchProfiles] Failed to fetch profiles from relay wss://fail.example: Error: relay timed out
|
||||
at Object.list (file:///app/tests/app-batch-fetch-profiles.test.mjs:80:31)
|
||||
at file:///app/js/utils/profileBatchFetcher.js:115:10
|
||||
at Array.map (<anonymous>)
|
||||
at batchFetchProfilesFromRelays (file:///app/js/utils/profileBatchFetcher.js:112:75)
|
||||
at TestContext.<anonymous> (file:///app/tests/app-batch-fetch-profiles.test.mjs:87:11)
|
||||
at Test.runInAsyncScope (node:async_hooks:214:14)
|
||||
at Test.run (node:internal/test_runner/test:1047:25)
|
||||
at Test.start (node:internal/test_runner/test:944:17)
|
||||
at startSubtestAfterBootstrap (node:internal/test_runner/harness:296:17)
|
||||
TAP version 13
|
||||
# Subtest: batchFetchProfiles handles fast and failing relays
|
||||
ok 1 - batchFetchProfiles handles fast and failing relays
|
||||
---
|
||||
duration_ms: 7.891372
|
||||
type: 'test'
|
||||
...
|
||||
1..1
|
||||
# tests 1
|
||||
# suites 0
|
||||
# pass 1
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 22.271669
|
||||
|
||||
→ Running tests/app/channel-profile-moderation.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
[feedEngine/moderation] metadata.moderation updated {
|
||||
stage: 'moderation',
|
||||
videoId: 'vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv',
|
||||
blockAutoplay: true,
|
||||
blurThumbnail: true,
|
||||
trustedCount: 2,
|
||||
reportType: 'nudity',
|
||||
hidden: false,
|
||||
hideReason: null,
|
||||
hideBypass: null
|
||||
}
|
||||
TAP version 13
|
||||
# Subtest: renderChannelVideosFromList decorates videos before storing
|
||||
ok 1 - renderChannelVideosFromList decorates videos before storing
|
||||
---
|
||||
duration_ms: 361.619024
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: renderChannelVideosFromList applies moderation blur without existing metadata
|
||||
ok 2 - renderChannelVideosFromList applies moderation blur without existing metadata
|
||||
---
|
||||
duration_ms: 40.463597
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: applyChannelVisualBlur blurs banner and avatar when viewer mutes author
|
||||
ok 3 - applyChannelVisualBlur blurs banner and avatar when viewer mutes author
|
||||
---
|
||||
duration_ms: 16.290213
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: moderation override clears channel blur via event wiring
|
||||
ok 4 - moderation override clears channel blur via event wiring
|
||||
---
|
||||
duration_ms: 42.036247
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: channel header moderation badge reflects blur state and override actions
|
||||
ok 5 - channel header moderation badge reflects blur state and override actions
|
||||
---
|
||||
duration_ms: 42.837872
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: channel video cards update moderation state in place when summary changes
|
||||
ok 6 - channel video cards update moderation state in place when summary changes
|
||||
---
|
||||
duration_ms: 26.383893
|
||||
type: 'test'
|
||||
...
|
||||
1..6
|
||||
# tests 6
|
||||
# suites 0
|
||||
# pass 6
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 542.904828
|
||||
|
||||
→ Running tests/app/hash-change-handler.test.mjs
|
||||
TAP version 13
|
||||
# Subtest: handleHashChange defaults to for-you when logged in
|
||||
ok 1 - handleHashChange defaults to for-you when logged in
|
||||
---
|
||||
duration_ms: 82.739827
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleHashChange defaults to most-recent-videos when logged out
|
||||
ok 2 - handleHashChange defaults to most-recent-videos when logged out
|
||||
---
|
||||
duration_ms: 21.613469
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleHashChange respects explicit view regardless of login state
|
||||
ok 3 - handleHashChange respects explicit view regardless of login state
|
||||
---
|
||||
duration_ms: 11.465331
|
||||
type: 'test'
|
||||
...
|
||||
1..3
|
||||
# tests 3
|
||||
# suites 0
|
||||
# pass 3
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 133.942749
|
||||
|
||||
→ Running tests/app/hydrate-sidebar-navigation.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: hydrateSidebarNavigation reveals chrome controls for authenticated viewers
|
||||
ok 1 - hydrateSidebarNavigation reveals chrome controls for authenticated viewers
|
||||
---
|
||||
duration_ms: 101.830503
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: hydrateSidebarNavigation hides chrome controls for logged-out viewers
|
||||
ok 2 - hydrateSidebarNavigation hides chrome controls for logged-out viewers
|
||||
---
|
||||
duration_ms: 26.06532
|
||||
type: 'test'
|
||||
...
|
||||
1..2
|
||||
# tests 2
|
||||
# suites 0
|
||||
# pass 2
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 143.762843
|
||||
|
||||
→ Running tests/app/is-user-logged-in.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: isUserLoggedIn returns false when no user pubkey is set
|
||||
ok 1 - isUserLoggedIn returns false when no user pubkey is set
|
||||
---
|
||||
duration_ms: 1.383277
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn treats extension logins as authenticated
|
||||
ok 2 - isUserLoggedIn treats extension logins as authenticated
|
||||
---
|
||||
duration_ms: 0.414559
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn guards against mismatched nostrClient state
|
||||
ok 3 - isUserLoggedIn guards against mismatched nostrClient state
|
||||
---
|
||||
duration_ms: 0.223376
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn ignores anonymous session actor mismatches
|
||||
ok 4 - isUserLoggedIn ignores anonymous session actor mismatches
|
||||
---
|
||||
duration_ms: 0.345963
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn rejects mismatched managed session actors
|
||||
ok 5 - isUserLoggedIn rejects mismatched managed session actors
|
||||
---
|
||||
duration_ms: 0.265368
|
||||
type: 'test'
|
||||
...
|
||||
1..5
|
||||
# tests 5
|
||||
# suites 0
|
||||
# pass 5
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 31.714232
|
||||
|
||||
→ Running tests/app/moderation-overrides.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: handleModerationOverride decorates stored and current videos then refreshes UI
|
||||
ok 1 - handleModerationOverride decorates stored and current videos then refreshes UI
|
||||
---
|
||||
duration_ms: 214.226302
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleModerationOverride resumes deferred playback
|
||||
ok 2 - handleModerationOverride resumes deferred playback
|
||||
---
|
||||
duration_ms: 1.308813
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleModerationBlock requests a block, clears overrides, and refreshes hidden state
|
||||
not ok 3 - handleModerationBlock requests a block, clears overrides, and refreshes hidden state
|
||||
---
|
||||
duration_ms: 4.950644
|
||||
type: 'test'
|
||||
location: '/app/tests/app/moderation-overrides.test.mjs:112:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
|
||||
false !== true
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: true
|
||||
actual: false
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///app/tests/app/moderation-overrides.test.mjs:228:10)
|
||||
async Test.run (node:internal/test_runner/test:1054:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
|
||||
...
|
||||
# Subtest: handleModerationBlock returns false when viewer is logged out
|
||||
ok 4 - handleModerationBlock returns false when viewer is logged out
|
||||
---
|
||||
duration_ms: 1.626768
|
||||
type: 'test'
|
||||
...
|
||||
1..4
|
||||
# tests 4
|
||||
# suites 0
|
||||
# pass 3
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 254.906261
|
||||
file:///app/scripts/run-unit-tests.mjs:68
|
||||
reject(new Error(`${relativePath} failed with exit code ${code}`));
|
||||
^
|
||||
|
||||
Error: tests/app/moderation-overrides.test.mjs failed with exit code 1
|
||||
at ChildProcess.<anonymous> (file:///app/scripts/run-unit-tests.mjs:68:16)
|
||||
at ChildProcess.emit (node:events:519:28)
|
||||
at maybeClose (node:internal/child_process:1101:16)
|
||||
at ChildProcess._handle.onexit (node:internal/child_process:304:5)
|
||||
|
||||
Node.js v22.21.1
|
||||
@@ -1,260 +0,0 @@
|
||||
|
||||
> [email protected] test:unit
|
||||
> node scripts/run-unit-tests.mjs
|
||||
|
||||
|
||||
→ Running tests/app-batch-fetch-profiles.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
[batchFetchProfiles] Failed to fetch profiles from relay wss://fail.example: Error: relay timed out
|
||||
at Object.list (file:///app/tests/app-batch-fetch-profiles.test.mjs:80:31)
|
||||
at file:///app/js/utils/profileBatchFetcher.js:115:10
|
||||
at Array.map (<anonymous>)
|
||||
at batchFetchProfilesFromRelays (file:///app/js/utils/profileBatchFetcher.js:112:75)
|
||||
at TestContext.<anonymous> (file:///app/tests/app-batch-fetch-profiles.test.mjs:87:11)
|
||||
at Test.runInAsyncScope (node:async_hooks:214:14)
|
||||
at Test.run (node:internal/test_runner/test:1047:25)
|
||||
at Test.start (node:internal/test_runner/test:944:17)
|
||||
at startSubtestAfterBootstrap (node:internal/test_runner/harness:296:17)
|
||||
TAP version 13
|
||||
# Subtest: batchFetchProfiles handles fast and failing relays
|
||||
ok 1 - batchFetchProfiles handles fast and failing relays
|
||||
---
|
||||
duration_ms: 4.89647
|
||||
type: 'test'
|
||||
...
|
||||
1..1
|
||||
# tests 1
|
||||
# suites 0
|
||||
# pass 1
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 21.69378
|
||||
|
||||
→ Running tests/app/channel-profile-moderation.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
[feedEngine/moderation] metadata.moderation updated {
|
||||
stage: 'moderation',
|
||||
videoId: 'vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv',
|
||||
blockAutoplay: true,
|
||||
blurThumbnail: true,
|
||||
trustedCount: 2,
|
||||
reportType: 'nudity',
|
||||
hidden: false,
|
||||
hideReason: null,
|
||||
hideBypass: null
|
||||
}
|
||||
TAP version 13
|
||||
# Subtest: renderChannelVideosFromList decorates videos before storing
|
||||
ok 1 - renderChannelVideosFromList decorates videos before storing
|
||||
---
|
||||
duration_ms: 401.108042
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: renderChannelVideosFromList applies moderation blur without existing metadata
|
||||
ok 2 - renderChannelVideosFromList applies moderation blur without existing metadata
|
||||
---
|
||||
duration_ms: 46.542954
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: applyChannelVisualBlur blurs banner and avatar when viewer mutes author
|
||||
ok 3 - applyChannelVisualBlur blurs banner and avatar when viewer mutes author
|
||||
---
|
||||
duration_ms: 15.658716
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: moderation override clears channel blur via event wiring
|
||||
ok 4 - moderation override clears channel blur via event wiring
|
||||
---
|
||||
duration_ms: 52.358724
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: channel header moderation badge reflects blur state and override actions
|
||||
ok 5 - channel header moderation badge reflects blur state and override actions
|
||||
---
|
||||
duration_ms: 41.743826
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: channel video cards update moderation state in place when summary changes
|
||||
ok 6 - channel video cards update moderation state in place when summary changes
|
||||
---
|
||||
duration_ms: 26.080671
|
||||
type: 'test'
|
||||
...
|
||||
1..6
|
||||
# tests 6
|
||||
# suites 0
|
||||
# pass 6
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 611.793776
|
||||
|
||||
→ Running tests/app/hash-change-handler.test.mjs
|
||||
TAP version 13
|
||||
# Subtest: handleHashChange defaults to for-you when logged in
|
||||
ok 1 - handleHashChange defaults to for-you when logged in
|
||||
---
|
||||
duration_ms: 78.541617
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleHashChange defaults to most-recent-videos when logged out
|
||||
ok 2 - handleHashChange defaults to most-recent-videos when logged out
|
||||
---
|
||||
duration_ms: 10.259655
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleHashChange respects explicit view regardless of login state
|
||||
ok 3 - handleHashChange respects explicit view regardless of login state
|
||||
---
|
||||
duration_ms: 10.116444
|
||||
type: 'test'
|
||||
...
|
||||
1..3
|
||||
# tests 3
|
||||
# suites 0
|
||||
# pass 3
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 112.397132
|
||||
|
||||
→ Running tests/app/hydrate-sidebar-navigation.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: hydrateSidebarNavigation reveals chrome controls for authenticated viewers
|
||||
ok 1 - hydrateSidebarNavigation reveals chrome controls for authenticated viewers
|
||||
---
|
||||
duration_ms: 98.541032
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: hydrateSidebarNavigation hides chrome controls for logged-out viewers
|
||||
ok 2 - hydrateSidebarNavigation hides chrome controls for logged-out viewers
|
||||
---
|
||||
duration_ms: 15.672259
|
||||
type: 'test'
|
||||
...
|
||||
1..2
|
||||
# tests 2
|
||||
# suites 0
|
||||
# pass 2
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 131.789275
|
||||
|
||||
→ Running tests/app/is-user-logged-in.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: isUserLoggedIn returns false when no user pubkey is set
|
||||
ok 1 - isUserLoggedIn returns false when no user pubkey is set
|
||||
---
|
||||
duration_ms: 1.459953
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn treats extension logins as authenticated
|
||||
ok 2 - isUserLoggedIn treats extension logins as authenticated
|
||||
---
|
||||
duration_ms: 0.427722
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn guards against mismatched nostrClient state
|
||||
ok 3 - isUserLoggedIn guards against mismatched nostrClient state
|
||||
---
|
||||
duration_ms: 0.232365
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn ignores anonymous session actor mismatches
|
||||
ok 4 - isUserLoggedIn ignores anonymous session actor mismatches
|
||||
---
|
||||
duration_ms: 0.295374
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn rejects mismatched managed session actors
|
||||
ok 5 - isUserLoggedIn rejects mismatched managed session actors
|
||||
---
|
||||
duration_ms: 0.263757
|
||||
type: 'test'
|
||||
...
|
||||
1..5
|
||||
# tests 5
|
||||
# suites 0
|
||||
# pass 5
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 32.877837
|
||||
|
||||
→ Running tests/app/moderation-overrides.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: handleModerationOverride decorates stored and current videos then refreshes UI
|
||||
ok 1 - handleModerationOverride decorates stored and current videos then refreshes UI
|
||||
---
|
||||
duration_ms: 230.312105
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleModerationOverride resumes deferred playback
|
||||
ok 2 - handleModerationOverride resumes deferred playback
|
||||
---
|
||||
duration_ms: 1.320201
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleModerationBlock requests a block, clears overrides, and refreshes hidden state
|
||||
not ok 3 - handleModerationBlock requests a block, clears overrides, and refreshes hidden state
|
||||
---
|
||||
duration_ms: 4.990623
|
||||
type: 'test'
|
||||
location: '/app/tests/app/moderation-overrides.test.mjs:112:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
|
||||
false !== true
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: true
|
||||
actual: false
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///app/tests/app/moderation-overrides.test.mjs:228:10)
|
||||
async Test.run (node:internal/test_runner/test:1054:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
|
||||
...
|
||||
# Subtest: handleModerationBlock returns false when viewer is logged out
|
||||
ok 4 - handleModerationBlock returns false when viewer is logged out
|
||||
---
|
||||
duration_ms: 1.778329
|
||||
type: 'test'
|
||||
...
|
||||
1..4
|
||||
# tests 4
|
||||
# suites 0
|
||||
# pass 3
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 271.042194
|
||||
file:///app/scripts/run-unit-tests.mjs:68
|
||||
reject(new Error(`${relativePath} failed with exit code ${code}`));
|
||||
^
|
||||
|
||||
Error: tests/app/moderation-overrides.test.mjs failed with exit code 1
|
||||
at ChildProcess.<anonymous> (file:///app/scripts/run-unit-tests.mjs:68:16)
|
||||
at ChildProcess.emit (node:events:519:28)
|
||||
at maybeClose (node:internal/child_process:1101:16)
|
||||
at ChildProcess._handle.onexit (node:internal/child_process:304:5)
|
||||
|
||||
Node.js v22.21.1
|
||||
@@ -1,305 +0,0 @@
|
||||
|
||||
> [email protected] test:unit
|
||||
> node scripts/run-unit-tests.mjs
|
||||
|
||||
|
||||
→ Running tests/app-batch-fetch-profiles.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
[batchFetchProfiles] Failed to fetch profiles from relay wss://fail.example: Error: relay timed out
|
||||
at Object.list (file:///app/tests/app-batch-fetch-profiles.test.mjs:80:31)
|
||||
at file:///app/js/utils/profileBatchFetcher.js:115:10
|
||||
at Array.map (<anonymous>)
|
||||
at batchFetchProfilesFromRelays (file:///app/js/utils/profileBatchFetcher.js:112:75)
|
||||
at TestContext.<anonymous> (file:///app/tests/app-batch-fetch-profiles.test.mjs:87:11)
|
||||
at Test.runInAsyncScope (node:async_hooks:214:14)
|
||||
at Test.run (node:internal/test_runner/test:1047:25)
|
||||
at Test.start (node:internal/test_runner/test:944:17)
|
||||
at startSubtestAfterBootstrap (node:internal/test_runner/harness:296:17)
|
||||
TAP version 13
|
||||
# Subtest: batchFetchProfiles handles fast and failing relays
|
||||
ok 1 - batchFetchProfiles handles fast and failing relays
|
||||
---
|
||||
duration_ms: 7.977402
|
||||
type: 'test'
|
||||
...
|
||||
1..1
|
||||
# tests 1
|
||||
# suites 0
|
||||
# pass 1
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 22.241514
|
||||
|
||||
→ Running tests/app/channel-profile-moderation.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
[feedEngine/moderation] metadata.moderation updated {
|
||||
stage: 'moderation',
|
||||
videoId: 'vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv',
|
||||
blockAutoplay: true,
|
||||
blurThumbnail: true,
|
||||
trustedCount: 2,
|
||||
reportType: 'nudity',
|
||||
hidden: false,
|
||||
hideReason: null,
|
||||
hideBypass: null
|
||||
}
|
||||
TAP version 13
|
||||
# Subtest: renderChannelVideosFromList decorates videos before storing
|
||||
ok 1 - renderChannelVideosFromList decorates videos before storing
|
||||
---
|
||||
duration_ms: 379.80434
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: renderChannelVideosFromList applies moderation blur without existing metadata
|
||||
ok 2 - renderChannelVideosFromList applies moderation blur without existing metadata
|
||||
---
|
||||
duration_ms: 45.289258
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: applyChannelVisualBlur blurs banner and avatar when viewer mutes author
|
||||
ok 3 - applyChannelVisualBlur blurs banner and avatar when viewer mutes author
|
||||
---
|
||||
duration_ms: 15.951023
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: moderation override clears channel blur via event wiring
|
||||
ok 4 - moderation override clears channel blur via event wiring
|
||||
---
|
||||
duration_ms: 45.789308
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: channel header moderation badge reflects blur state and override actions
|
||||
ok 5 - channel header moderation badge reflects blur state and override actions
|
||||
---
|
||||
duration_ms: 42.447356
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: channel video cards update moderation state in place when summary changes
|
||||
ok 6 - channel video cards update moderation state in place when summary changes
|
||||
---
|
||||
duration_ms: 25.597118
|
||||
type: 'test'
|
||||
...
|
||||
1..6
|
||||
# tests 6
|
||||
# suites 0
|
||||
# pass 6
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 568.177635
|
||||
|
||||
→ Running tests/app/hash-change-handler.test.mjs
|
||||
TAP version 13
|
||||
# Subtest: handleHashChange defaults to for-you when logged in
|
||||
ok 1 - handleHashChange defaults to for-you when logged in
|
||||
---
|
||||
duration_ms: 75.772174
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleHashChange defaults to most-recent-videos when logged out
|
||||
ok 2 - handleHashChange defaults to most-recent-videos when logged out
|
||||
---
|
||||
duration_ms: 9.627833
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleHashChange respects explicit view regardless of login state
|
||||
ok 3 - handleHashChange respects explicit view regardless of login state
|
||||
---
|
||||
duration_ms: 10.030577
|
||||
type: 'test'
|
||||
...
|
||||
1..3
|
||||
# tests 3
|
||||
# suites 0
|
||||
# pass 3
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 108.647336
|
||||
|
||||
→ Running tests/app/hydrate-sidebar-navigation.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: hydrateSidebarNavigation reveals chrome controls for authenticated viewers
|
||||
ok 1 - hydrateSidebarNavigation reveals chrome controls for authenticated viewers
|
||||
---
|
||||
duration_ms: 118.594258
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: hydrateSidebarNavigation hides chrome controls for logged-out viewers
|
||||
ok 2 - hydrateSidebarNavigation hides chrome controls for logged-out viewers
|
||||
---
|
||||
duration_ms: 27.333658
|
||||
type: 'test'
|
||||
...
|
||||
1..2
|
||||
# tests 2
|
||||
# suites 0
|
||||
# pass 2
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 161.988571
|
||||
|
||||
→ Running tests/app/is-user-logged-in.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: isUserLoggedIn returns false when no user pubkey is set
|
||||
ok 1 - isUserLoggedIn returns false when no user pubkey is set
|
||||
---
|
||||
duration_ms: 1.780936
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn treats extension logins as authenticated
|
||||
ok 2 - isUserLoggedIn treats extension logins as authenticated
|
||||
---
|
||||
duration_ms: 0.469339
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn guards against mismatched nostrClient state
|
||||
ok 3 - isUserLoggedIn guards against mismatched nostrClient state
|
||||
---
|
||||
duration_ms: 0.229676
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn ignores anonymous session actor mismatches
|
||||
ok 4 - isUserLoggedIn ignores anonymous session actor mismatches
|
||||
---
|
||||
duration_ms: 0.274654
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn rejects mismatched managed session actors
|
||||
ok 5 - isUserLoggedIn rejects mismatched managed session actors
|
||||
---
|
||||
duration_ms: 0.278603
|
||||
type: 'test'
|
||||
...
|
||||
1..5
|
||||
# tests 5
|
||||
# suites 0
|
||||
# pass 5
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 35.209771
|
||||
|
||||
→ Running tests/app/moderation-overrides.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: handleModerationOverride decorates stored and current videos then refreshes UI
|
||||
not ok 1 - handleModerationOverride decorates stored and current videos then refreshes UI
|
||||
---
|
||||
duration_ms: 228.266794
|
||||
type: 'test'
|
||||
location: '/app/tests/app/moderation-overrides.test.mjs:27:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ undefined
|
||||
- true
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: true
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///app/tests/app/moderation-overrides.test.mjs:67:10)
|
||||
async Test.run (node:internal/test_runner/test:1054:7)
|
||||
async startSubtestAfterBootstrap (node:internal/test_runner/harness:296:3)
|
||||
...
|
||||
# Subtest: handleModerationOverride resumes deferred playback
|
||||
not ok 2 - handleModerationOverride resumes deferred playback
|
||||
---
|
||||
duration_ms: 2.019286
|
||||
type: 'test'
|
||||
location: '/app/tests/app/moderation-overrides.test.mjs:75:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ {
|
||||
+ magnet: '',
|
||||
+ triggerProvided: false,
|
||||
+ url: 'https://example.com/video.mp4',
|
||||
+ videoId: 'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd'
|
||||
+ }
|
||||
- null
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: ~
|
||||
actual:
|
||||
url: 'https://example.com/video.mp4'
|
||||
magnet: ''
|
||||
triggerProvided: false
|
||||
videoId: 'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd'
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///app/tests/app/moderation-overrides.test.mjs:104:10)
|
||||
async Test.run (node:internal/test_runner/test:1054:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
|
||||
...
|
||||
# Subtest: handleModerationBlock requests a block, clears overrides, and refreshes hidden state
|
||||
not ok 3 - handleModerationBlock requests a block, clears overrides, and refreshes hidden state
|
||||
---
|
||||
duration_ms: 2.784959
|
||||
type: 'test'
|
||||
location: '/app/tests/app/moderation-overrides.test.mjs:112:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ undefined
|
||||
- true
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: true
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///app/tests/app/moderation-overrides.test.mjs:202:10)
|
||||
async Test.run (node:internal/test_runner/test:1054:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
|
||||
...
|
||||
# Subtest: handleModerationBlock returns false when viewer is logged out
|
||||
ok 4 - handleModerationBlock returns false when viewer is logged out
|
||||
---
|
||||
duration_ms: 2.057327
|
||||
type: 'test'
|
||||
...
|
||||
1..4
|
||||
# tests 4
|
||||
# suites 0
|
||||
# pass 1
|
||||
# fail 3
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 267.221529
|
||||
file:///app/scripts/run-unit-tests.mjs:68
|
||||
reject(new Error(`${relativePath} failed with exit code ${code}`));
|
||||
^
|
||||
|
||||
Error: tests/app/moderation-overrides.test.mjs failed with exit code 1
|
||||
at ChildProcess.<anonymous> (file:///app/scripts/run-unit-tests.mjs:68:16)
|
||||
at ChildProcess.emit (node:events:519:28)
|
||||
at maybeClose (node:internal/child_process:1101:16)
|
||||
at ChildProcess._handle.onexit (node:internal/child_process:304:5)
|
||||
|
||||
Node.js v22.21.1
|
||||
@@ -1,287 +0,0 @@
|
||||
|
||||
> [email protected] test:unit
|
||||
> node scripts/run-unit-tests.mjs
|
||||
|
||||
|
||||
→ Running tests/app-batch-fetch-profiles.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
[batchFetchProfiles] Failed to fetch profiles from relay wss://fail.example: Error: relay timed out
|
||||
at Object.list (file:///app/tests/app-batch-fetch-profiles.test.mjs:80:31)
|
||||
at file:///app/js/utils/profileBatchFetcher.js:115:10
|
||||
at Array.map (<anonymous>)
|
||||
at batchFetchProfilesFromRelays (file:///app/js/utils/profileBatchFetcher.js:112:75)
|
||||
at TestContext.<anonymous> (file:///app/tests/app-batch-fetch-profiles.test.mjs:87:11)
|
||||
at Test.runInAsyncScope (node:async_hooks:214:14)
|
||||
at Test.run (node:internal/test_runner/test:1047:25)
|
||||
at Test.start (node:internal/test_runner/test:944:17)
|
||||
at startSubtestAfterBootstrap (node:internal/test_runner/harness:296:17)
|
||||
TAP version 13
|
||||
# Subtest: batchFetchProfiles handles fast and failing relays
|
||||
ok 1 - batchFetchProfiles handles fast and failing relays
|
||||
---
|
||||
duration_ms: 4.68097
|
||||
type: 'test'
|
||||
...
|
||||
1..1
|
||||
# tests 1
|
||||
# suites 0
|
||||
# pass 1
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 18.751403
|
||||
|
||||
→ Running tests/app/channel-profile-moderation.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
[feedEngine/moderation] metadata.moderation updated {
|
||||
stage: 'moderation',
|
||||
videoId: 'vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv',
|
||||
blockAutoplay: true,
|
||||
blurThumbnail: true,
|
||||
trustedCount: 2,
|
||||
reportType: 'nudity',
|
||||
hidden: false,
|
||||
hideReason: null,
|
||||
hideBypass: null
|
||||
}
|
||||
TAP version 13
|
||||
# Subtest: renderChannelVideosFromList decorates videos before storing
|
||||
ok 1 - renderChannelVideosFromList decorates videos before storing
|
||||
---
|
||||
duration_ms: 249.242079
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: renderChannelVideosFromList applies moderation blur without existing metadata
|
||||
ok 2 - renderChannelVideosFromList applies moderation blur without existing metadata
|
||||
---
|
||||
duration_ms: 34.522355
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: applyChannelVisualBlur blurs banner and avatar when viewer mutes author
|
||||
ok 3 - applyChannelVisualBlur blurs banner and avatar when viewer mutes author
|
||||
---
|
||||
duration_ms: 11.527787
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: moderation override clears channel blur via event wiring
|
||||
ok 4 - moderation override clears channel blur via event wiring
|
||||
---
|
||||
duration_ms: 39.046527
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: channel header moderation badge reflects blur state and override actions
|
||||
ok 5 - channel header moderation badge reflects blur state and override actions
|
||||
---
|
||||
duration_ms: 34.441459
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: channel video cards update moderation state in place when summary changes
|
||||
ok 6 - channel video cards update moderation state in place when summary changes
|
||||
---
|
||||
duration_ms: 23.360676
|
||||
type: 'test'
|
||||
...
|
||||
1..6
|
||||
# tests 6
|
||||
# suites 0
|
||||
# pass 6
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 404.34229
|
||||
|
||||
→ Running tests/app/hash-change-handler.test.mjs
|
||||
TAP version 13
|
||||
# Subtest: handleHashChange defaults to for-you when logged in
|
||||
ok 1 - handleHashChange defaults to for-you when logged in
|
||||
---
|
||||
duration_ms: 1009.599136
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleHashChange defaults to most-recent-videos when logged out
|
||||
ok 2 - handleHashChange defaults to most-recent-videos when logged out
|
||||
---
|
||||
duration_ms: 73.745699
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleHashChange respects explicit view regardless of login state
|
||||
ok 3 - handleHashChange respects explicit view regardless of login state
|
||||
---
|
||||
duration_ms: 33.538414
|
||||
type: 'test'
|
||||
...
|
||||
1..3
|
||||
# tests 3
|
||||
# suites 0
|
||||
# pass 3
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 1164.107182
|
||||
|
||||
→ Running tests/app/hydrate-sidebar-navigation.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: hydrateSidebarNavigation reveals chrome controls for authenticated viewers
|
||||
ok 1 - hydrateSidebarNavigation reveals chrome controls for authenticated viewers
|
||||
---
|
||||
duration_ms: 123.254594
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: hydrateSidebarNavigation hides chrome controls for logged-out viewers
|
||||
ok 2 - hydrateSidebarNavigation hides chrome controls for logged-out viewers
|
||||
---
|
||||
duration_ms: 17.578009
|
||||
type: 'test'
|
||||
...
|
||||
1..2
|
||||
# tests 2
|
||||
# suites 0
|
||||
# pass 2
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 173.980931
|
||||
|
||||
→ Running tests/app/is-user-logged-in.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: isUserLoggedIn returns false when no user pubkey is set
|
||||
ok 1 - isUserLoggedIn returns false when no user pubkey is set
|
||||
---
|
||||
duration_ms: 1.547337
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn treats extension logins as authenticated
|
||||
ok 2 - isUserLoggedIn treats extension logins as authenticated
|
||||
---
|
||||
duration_ms: 0.411532
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn guards against mismatched nostrClient state
|
||||
ok 3 - isUserLoggedIn guards against mismatched nostrClient state
|
||||
---
|
||||
duration_ms: 0.230064
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn ignores anonymous session actor mismatches
|
||||
ok 4 - isUserLoggedIn ignores anonymous session actor mismatches
|
||||
---
|
||||
duration_ms: 0.402138
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn rejects mismatched managed session actors
|
||||
ok 5 - isUserLoggedIn rejects mismatched managed session actors
|
||||
---
|
||||
duration_ms: 0.24381
|
||||
type: 'test'
|
||||
...
|
||||
1..5
|
||||
# tests 5
|
||||
# suites 0
|
||||
# pass 5
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 48.612244
|
||||
|
||||
→ Running tests/app/moderation-overrides.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: handleModerationOverride decorates stored and current videos then refreshes UI
|
||||
ok 1 - handleModerationOverride decorates stored and current videos then refreshes UI
|
||||
---
|
||||
duration_ms: 244.260716
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleModerationOverride resumes deferred playback
|
||||
not ok 2 - handleModerationOverride resumes deferred playback
|
||||
---
|
||||
duration_ms: 3.378509
|
||||
type: 'test'
|
||||
location: '/app/tests/app/moderation-overrides.test.mjs:75:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ {
|
||||
+ magnet: '',
|
||||
+ triggerProvided: false,
|
||||
+ url: 'https://example.com/video.mp4',
|
||||
+ videoId: 'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd'
|
||||
+ }
|
||||
- null
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: ~
|
||||
actual:
|
||||
url: 'https://example.com/video.mp4'
|
||||
magnet: ''
|
||||
triggerProvided: false
|
||||
videoId: 'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd'
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///app/tests/app/moderation-overrides.test.mjs:104:10)
|
||||
async Test.run (node:internal/test_runner/test:1054:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
|
||||
...
|
||||
# Subtest: handleModerationBlock requests a block, clears overrides, and refreshes hidden state
|
||||
not ok 3 - handleModerationBlock requests a block, clears overrides, and refreshes hidden state
|
||||
---
|
||||
duration_ms: 4.927295
|
||||
type: 'test'
|
||||
location: '/app/tests/app/moderation-overrides.test.mjs:112:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
|
||||
false !== true
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: true
|
||||
actual: false
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///app/tests/app/moderation-overrides.test.mjs:228:10)
|
||||
async Test.run (node:internal/test_runner/test:1054:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
|
||||
...
|
||||
# Subtest: handleModerationBlock returns false when viewer is logged out
|
||||
ok 4 - handleModerationBlock returns false when viewer is logged out
|
||||
---
|
||||
duration_ms: 1.922279
|
||||
type: 'test'
|
||||
...
|
||||
1..4
|
||||
# tests 4
|
||||
# suites 0
|
||||
# pass 2
|
||||
# fail 2
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 287.75859
|
||||
file:///app/scripts/run-unit-tests.mjs:68
|
||||
reject(new Error(`${relativePath} failed with exit code ${code}`));
|
||||
^
|
||||
|
||||
Error: tests/app/moderation-overrides.test.mjs failed with exit code 1
|
||||
at ChildProcess.<anonymous> (file:///app/scripts/run-unit-tests.mjs:68:16)
|
||||
at ChildProcess.emit (node:events:519:28)
|
||||
at maybeClose (node:internal/child_process:1101:16)
|
||||
at ChildProcess._handle.onexit (node:internal/child_process:304:5)
|
||||
|
||||
Node.js v22.21.1
|
||||
@@ -1,260 +0,0 @@
|
||||
|
||||
> [email protected] test:unit
|
||||
> node scripts/run-unit-tests.mjs
|
||||
|
||||
|
||||
→ Running tests/app-batch-fetch-profiles.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
[batchFetchProfiles] Failed to fetch profiles from relay wss://fail.example: Error: relay timed out
|
||||
at Object.list (file:///app/tests/app-batch-fetch-profiles.test.mjs:80:31)
|
||||
at file:///app/js/utils/profileBatchFetcher.js:115:10
|
||||
at Array.map (<anonymous>)
|
||||
at batchFetchProfilesFromRelays (file:///app/js/utils/profileBatchFetcher.js:112:75)
|
||||
at TestContext.<anonymous> (file:///app/tests/app-batch-fetch-profiles.test.mjs:87:11)
|
||||
at Test.runInAsyncScope (node:async_hooks:214:14)
|
||||
at Test.run (node:internal/test_runner/test:1047:25)
|
||||
at Test.start (node:internal/test_runner/test:944:17)
|
||||
at startSubtestAfterBootstrap (node:internal/test_runner/harness:296:17)
|
||||
TAP version 13
|
||||
# Subtest: batchFetchProfiles handles fast and failing relays
|
||||
ok 1 - batchFetchProfiles handles fast and failing relays
|
||||
---
|
||||
duration_ms: 6.00491
|
||||
type: 'test'
|
||||
...
|
||||
1..1
|
||||
# tests 1
|
||||
# suites 0
|
||||
# pass 1
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 32.022592
|
||||
|
||||
→ Running tests/app/channel-profile-moderation.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
[feedEngine/moderation] metadata.moderation updated {
|
||||
stage: 'moderation',
|
||||
videoId: 'vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv',
|
||||
blockAutoplay: true,
|
||||
blurThumbnail: true,
|
||||
trustedCount: 2,
|
||||
reportType: 'nudity',
|
||||
hidden: false,
|
||||
hideReason: null,
|
||||
hideBypass: null
|
||||
}
|
||||
TAP version 13
|
||||
# Subtest: renderChannelVideosFromList decorates videos before storing
|
||||
ok 1 - renderChannelVideosFromList decorates videos before storing
|
||||
---
|
||||
duration_ms: 375.346278
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: renderChannelVideosFromList applies moderation blur without existing metadata
|
||||
ok 2 - renderChannelVideosFromList applies moderation blur without existing metadata
|
||||
---
|
||||
duration_ms: 37.760277
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: applyChannelVisualBlur blurs banner and avatar when viewer mutes author
|
||||
ok 3 - applyChannelVisualBlur blurs banner and avatar when viewer mutes author
|
||||
---
|
||||
duration_ms: 14.528741
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: moderation override clears channel blur via event wiring
|
||||
ok 4 - moderation override clears channel blur via event wiring
|
||||
---
|
||||
duration_ms: 47.192924
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: channel header moderation badge reflects blur state and override actions
|
||||
ok 5 - channel header moderation badge reflects blur state and override actions
|
||||
---
|
||||
duration_ms: 39.987443
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: channel video cards update moderation state in place when summary changes
|
||||
ok 6 - channel video cards update moderation state in place when summary changes
|
||||
---
|
||||
duration_ms: 24.837088
|
||||
type: 'test'
|
||||
...
|
||||
1..6
|
||||
# tests 6
|
||||
# suites 0
|
||||
# pass 6
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 553.202787
|
||||
|
||||
→ Running tests/app/hash-change-handler.test.mjs
|
||||
TAP version 13
|
||||
# Subtest: handleHashChange defaults to for-you when logged in
|
||||
ok 1 - handleHashChange defaults to for-you when logged in
|
||||
---
|
||||
duration_ms: 73.049804
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleHashChange defaults to most-recent-videos when logged out
|
||||
ok 2 - handleHashChange defaults to most-recent-videos when logged out
|
||||
---
|
||||
duration_ms: 9.496508
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleHashChange respects explicit view regardless of login state
|
||||
ok 3 - handleHashChange respects explicit view regardless of login state
|
||||
---
|
||||
duration_ms: 9.841895
|
||||
type: 'test'
|
||||
...
|
||||
1..3
|
||||
# tests 3
|
||||
# suites 0
|
||||
# pass 3
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 105.172847
|
||||
|
||||
→ Running tests/app/hydrate-sidebar-navigation.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: hydrateSidebarNavigation reveals chrome controls for authenticated viewers
|
||||
ok 1 - hydrateSidebarNavigation reveals chrome controls for authenticated viewers
|
||||
---
|
||||
duration_ms: 91.833717
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: hydrateSidebarNavigation hides chrome controls for logged-out viewers
|
||||
ok 2 - hydrateSidebarNavigation hides chrome controls for logged-out viewers
|
||||
---
|
||||
duration_ms: 13.635597
|
||||
type: 'test'
|
||||
...
|
||||
1..2
|
||||
# tests 2
|
||||
# suites 0
|
||||
# pass 2
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 122.580199
|
||||
|
||||
→ Running tests/app/is-user-logged-in.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: isUserLoggedIn returns false when no user pubkey is set
|
||||
ok 1 - isUserLoggedIn returns false when no user pubkey is set
|
||||
---
|
||||
duration_ms: 1.302074
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn treats extension logins as authenticated
|
||||
ok 2 - isUserLoggedIn treats extension logins as authenticated
|
||||
---
|
||||
duration_ms: 0.407277
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn guards against mismatched nostrClient state
|
||||
ok 3 - isUserLoggedIn guards against mismatched nostrClient state
|
||||
---
|
||||
duration_ms: 0.283539
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn ignores anonymous session actor mismatches
|
||||
ok 4 - isUserLoggedIn ignores anonymous session actor mismatches
|
||||
---
|
||||
duration_ms: 0.252445
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: isUserLoggedIn rejects mismatched managed session actors
|
||||
ok 5 - isUserLoggedIn rejects mismatched managed session actors
|
||||
---
|
||||
duration_ms: 0.383152
|
||||
type: 'test'
|
||||
...
|
||||
1..5
|
||||
# tests 5
|
||||
# suites 0
|
||||
# pass 5
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 34.198852
|
||||
|
||||
→ Running tests/app/moderation-overrides.test.mjs
|
||||
[bitvid] Initialized nostr nip04 helpers.
|
||||
[bitvid] Initialized nostr nip44 helpers.
|
||||
TAP version 13
|
||||
# Subtest: handleModerationOverride decorates stored and current videos then refreshes UI
|
||||
ok 1 - handleModerationOverride decorates stored and current videos then refreshes UI
|
||||
---
|
||||
duration_ms: 210.938803
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleModerationOverride resumes deferred playback
|
||||
ok 2 - handleModerationOverride resumes deferred playback
|
||||
---
|
||||
duration_ms: 1.300514
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handleModerationBlock requests a block, clears overrides, and refreshes hidden state
|
||||
not ok 3 - handleModerationBlock requests a block, clears overrides, and refreshes hidden state
|
||||
---
|
||||
duration_ms: 4.873851
|
||||
type: 'test'
|
||||
location: '/app/tests/app/moderation-overrides.test.mjs:112:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
|
||||
false !== true
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: true
|
||||
actual: false
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///app/tests/app/moderation-overrides.test.mjs:228:10)
|
||||
async Test.run (node:internal/test_runner/test:1054:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
|
||||
...
|
||||
# Subtest: handleModerationBlock returns false when viewer is logged out
|
||||
ok 4 - handleModerationBlock returns false when viewer is logged out
|
||||
---
|
||||
duration_ms: 1.847791
|
||||
type: 'test'
|
||||
...
|
||||
1..4
|
||||
# tests 4
|
||||
# suites 0
|
||||
# pass 3
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 253.404435
|
||||
file:///app/scripts/run-unit-tests.mjs:68
|
||||
reject(new Error(`${relativePath} failed with exit code ${code}`));
|
||||
^
|
||||
|
||||
Error: tests/app/moderation-overrides.test.mjs failed with exit code 1
|
||||
at ChildProcess.<anonymous> (file:///app/scripts/run-unit-tests.mjs:68:16)
|
||||
at ChildProcess.emit (node:events:519:28)
|
||||
at maybeClose (node:internal/child_process:1101:16)
|
||||
at ChildProcess._handle.onexit (node:internal/child_process:304:5)
|
||||
|
||||
Node.js v22.21.1
|
||||
@@ -271,6 +271,7 @@ export async function createModerationAppHarness(options = {}) {
|
||||
Application.prototype.normalizeModerationSettings;
|
||||
app.getActiveModerationThresholds =
|
||||
Application.prototype.getActiveModerationThresholds;
|
||||
app.normalizeHexPubkey = Application.prototype.normalizeHexPubkey;
|
||||
|
||||
if (options.userBlocks) {
|
||||
app.isAuthorBlocked = function (pubkey) {
|
||||
|
||||
@@ -313,12 +313,12 @@ test("NIP-07 login falls back when structured permissions fail", async () => {
|
||||
const pubkey = result.pubkey;
|
||||
assert.equal(pubkey, HEX_PUBKEY);
|
||||
assert.ok(env.enableCalls.length >= 2, "should retry with alternate payloads");
|
||||
const [objectCall, stringCall, plainCall] = env.enableCalls;
|
||||
const [objectCall, stringCall] = env.enableCalls;
|
||||
assert.ok(Array.isArray(objectCall?.permissions));
|
||||
assert.equal(
|
||||
typeof objectCall.permissions[0],
|
||||
"object",
|
||||
"structured permission request should be attempted",
|
||||
"structured permission request should be attempted first",
|
||||
);
|
||||
assert.ok(Array.isArray(stringCall?.permissions));
|
||||
assert.equal(
|
||||
@@ -356,22 +356,12 @@ test("NIP-07 login supports extensions that only allow enable() without payload"
|
||||
assert.equal(
|
||||
env.enableCalls.length,
|
||||
3,
|
||||
"should fall back to plain enable() after payload rejections",
|
||||
"should fall back to plain enable() after explicit variants fail",
|
||||
);
|
||||
const [objectCall, stringCall, plainCall] = env.enableCalls;
|
||||
assert.ok(Array.isArray(objectCall?.permissions));
|
||||
assert.equal(
|
||||
typeof objectCall.permissions[0],
|
||||
"object",
|
||||
"structured permissions should be attempted before falling back",
|
||||
);
|
||||
assert.ok(Array.isArray(stringCall?.permissions));
|
||||
assert.equal(
|
||||
typeof stringCall.permissions[0],
|
||||
"string",
|
||||
"string permissions should be attempted before plain enable()",
|
||||
);
|
||||
assert.equal(plainCall, undefined, "final attempt should use plain enable()");
|
||||
assert.equal(plainCall, undefined, "last attempt should use plain enable()");
|
||||
} finally {
|
||||
env.restore();
|
||||
nostrClient.logout();
|
||||
@@ -411,7 +401,7 @@ test("NIP-07 login quickly retries when a permission payload stalls", async () =
|
||||
duration < 500,
|
||||
`login should fall back quickly from stalled enable payloads (duration: ${duration}ms)`,
|
||||
);
|
||||
assert.equal(env.enableCalls.length, 2, "should attempt structured and string payload variants");
|
||||
assert.equal(env.enableCalls.length, 2, "should attempt structured and string payload variants before success");
|
||||
const [objectCall, stringCall] = env.enableCalls;
|
||||
assert.ok(Array.isArray(objectCall?.permissions));
|
||||
assert.equal(typeof objectCall.permissions[0], "object");
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import { test, describe, before, after, beforeEach, afterEach, mock } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { generateSecretKey, getPublicKey } from "nostr-tools";
|
||||
import "../test-helpers/setup-localstorage.mjs";
|
||||
|
||||
// Mock globals needed for nostr-tools or other libs
|
||||
if (!globalThis.crypto) {
|
||||
globalThis.crypto = {
|
||||
getRandomValues: (arr) => {
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
arr[i] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function bytesToHex(bytes) {
|
||||
return Buffer.from(bytes).toString('hex');
|
||||
}
|
||||
|
||||
// Helper to generate 32-byte hex string
|
||||
function generateHexId() {
|
||||
return bytesToHex(generateSecretKey());
|
||||
}
|
||||
|
||||
describe("dmSignalEvents", () => {
|
||||
let nostrClient;
|
||||
let dmSignalEvents;
|
||||
let originalPool;
|
||||
let originalRelays;
|
||||
let originalPubkey;
|
||||
let originalSessionActor;
|
||||
let signer;
|
||||
let getActiveSigner;
|
||||
|
||||
before(async () => {
|
||||
// Import modules
|
||||
const facadeModule = await import("../../js/nostrClientFacade.js");
|
||||
const registryModule = await import("../../js/nostrClientRegistry.js");
|
||||
dmSignalEvents = await import("../../js/nostr/dmSignalEvents.js");
|
||||
|
||||
nostrClient = facadeModule.nostrClient;
|
||||
getActiveSigner = registryModule.getActiveSigner;
|
||||
|
||||
// Generate keys
|
||||
const secret = generateSecretKey();
|
||||
const privateKey = bytesToHex(secret);
|
||||
const pubkey = getPublicKey(secret);
|
||||
|
||||
// Register signer to ensure getActiveSigner returns something
|
||||
await nostrClient.registerPrivateKeySigner({ privateKey, pubkey });
|
||||
signer = getActiveSigner();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
originalPool = nostrClient.pool;
|
||||
originalRelays = nostrClient.relays;
|
||||
originalPubkey = nostrClient.pubkey;
|
||||
originalSessionActor = nostrClient.sessionActor;
|
||||
|
||||
// Mock pool
|
||||
nostrClient.pool = {
|
||||
publish: mock.fn((urls, event) => {
|
||||
return {
|
||||
on: (event, cb) => {
|
||||
if (event === 'ok') setTimeout(cb, 0);
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
nostrClient.relays = ["wss://relay.mock"];
|
||||
nostrClient.writeRelays = ["wss://relay.mock"];
|
||||
|
||||
// Ensure we are logged in
|
||||
nostrClient.pubkey = signer.pubkey;
|
||||
|
||||
// Ensure we are NOT a session actor (which blocks signals)
|
||||
nostrClient.sessionActor = null;
|
||||
|
||||
// Mock signEvent on the signer
|
||||
// We keep the original signer object but replace the method
|
||||
signer.signEvent = mock.fn(async (evt) => ({ ...evt, sig: "mock-sig", id: "mock-id" }));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nostrClient.pool = originalPool;
|
||||
nostrClient.relays = originalRelays;
|
||||
nostrClient.pubkey = originalPubkey;
|
||||
nostrClient.sessionActor = originalSessionActor;
|
||||
mock.reset();
|
||||
});
|
||||
|
||||
test("publishDmReadReceipt should succeed", async () => {
|
||||
const eventId = generateHexId();
|
||||
const recipientPubkey = generateHexId();
|
||||
const payload = {
|
||||
eventId,
|
||||
recipientPubkey,
|
||||
messageKind: 4
|
||||
};
|
||||
|
||||
const result = await dmSignalEvents.publishDmReadReceipt(nostrClient, payload);
|
||||
|
||||
assert.strictEqual(result.ok, true);
|
||||
assert.strictEqual(signer.signEvent.mock.callCount(), 1);
|
||||
assert.strictEqual(nostrClient.pool.publish.mock.callCount(), 1);
|
||||
|
||||
const signedEvent = result.event;
|
||||
// Check tags
|
||||
const tags = signedEvent.tags;
|
||||
assert.ok(tags.find(t => t[0] === "e" && t[1] === eventId));
|
||||
assert.ok(tags.find(t => t[0] === "p" && t[1] === recipientPubkey));
|
||||
// Check 'k' tag for messageKind
|
||||
assert.ok(tags.find(t => t[0] === "k" && t[1] === "4"));
|
||||
|
||||
assert.strictEqual(signedEvent.content, "");
|
||||
});
|
||||
|
||||
test("publishDmReadReceipt should fail if session actor", async () => {
|
||||
// Set sessionActor to look like the current user
|
||||
nostrClient.sessionActor = { pubkey: nostrClient.pubkey };
|
||||
|
||||
const payload = {
|
||||
eventId: generateHexId(),
|
||||
recipientPubkey: generateHexId(),
|
||||
messageKind: 4
|
||||
};
|
||||
|
||||
const result = await dmSignalEvents.publishDmReadReceipt(nostrClient, payload);
|
||||
assert.strictEqual(result.ok, false);
|
||||
assert.strictEqual(result.error.code, "session-actor-publish-blocked");
|
||||
});
|
||||
|
||||
test("publishDmTypingIndicator should succeed", async () => {
|
||||
const recipientPubkey = generateHexId();
|
||||
const conversationEventId = generateHexId();
|
||||
const payload = {
|
||||
recipientPubkey,
|
||||
conversationEventId
|
||||
};
|
||||
|
||||
const result = await dmSignalEvents.publishDmTypingIndicator(nostrClient, payload);
|
||||
|
||||
assert.strictEqual(result.ok, true);
|
||||
assert.strictEqual(signer.signEvent.mock.callCount(), 1);
|
||||
|
||||
const signedEvent = result.event;
|
||||
// Check tags
|
||||
assert.ok(signedEvent.tags.find(t => t[0] === "p" && t[1] === recipientPubkey));
|
||||
assert.ok(signedEvent.tags.find(t => t[0] === "e" && t[1] === conversationEventId));
|
||||
});
|
||||
|
||||
test("should fail if no signer", async () => {
|
||||
// Temporarily break signer
|
||||
const originalSignEvent = signer.signEvent;
|
||||
signer.signEvent = undefined;
|
||||
|
||||
const payload = { eventId: generateHexId(), recipientPubkey: generateHexId() };
|
||||
const result = await dmSignalEvents.publishDmReadReceipt(nostrClient, payload);
|
||||
|
||||
assert.strictEqual(result.ok, false);
|
||||
assert.strictEqual(result.error, "auth-required");
|
||||
|
||||
signer.signEvent = originalSignEvent;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
import { test, describe, beforeEach, afterEach } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import "../test-helpers/setup-localstorage.mjs";
|
||||
import { SyncMetadataStore, __testExports } from "../../js/nostr/syncMetadataStore.js";
|
||||
|
||||
const { SYNC_METADATA_STORAGE_KEY } = __testExports;
|
||||
|
||||
describe("SyncMetadataStore", () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
test("should load from localStorage on initialization", () => {
|
||||
const data = [["key1", 1234567890]];
|
||||
localStorage.setItem(SYNC_METADATA_STORAGE_KEY, JSON.stringify(data));
|
||||
|
||||
const store = new SyncMetadataStore();
|
||||
// computeKey(kind, pubkey, dTag, relayUrl)
|
||||
// We don't know the components of "key1" but it should be in the map.
|
||||
// However, the class treats keys opaquely once computed.
|
||||
// Let's use internal map to verify if possible, or verify via public API if we know the key components.
|
||||
// Accessing private property? logic uses `this.metadata`.
|
||||
// We can't easily access `this.metadata` if it's not exposed.
|
||||
// But we can verify with `getLastSeen`.
|
||||
|
||||
// Wait, "key1" isn't a valid key format generated by computeKey, but load() just loads the map.
|
||||
// getLastSeen computes the key again.
|
||||
// So if we pre-populate with a valid key, we can test it.
|
||||
|
||||
const kind = 1;
|
||||
const pubkey = "pubkey1";
|
||||
const dTag = "dtag1";
|
||||
const relayUrl = "wss://relay1.com";
|
||||
const key = `${kind}:${pubkey}:${dTag}:${relayUrl}`;
|
||||
const timestamp = 1000;
|
||||
|
||||
localStorage.setItem(SYNC_METADATA_STORAGE_KEY, JSON.stringify([[key, timestamp]]));
|
||||
|
||||
const store2 = new SyncMetadataStore();
|
||||
assert.strictEqual(store2.getLastSeen(kind, pubkey, dTag, relayUrl), timestamp);
|
||||
});
|
||||
|
||||
test("should update last seen and save to localStorage", () => {
|
||||
const store = new SyncMetadataStore();
|
||||
const kind = 1;
|
||||
const pubkey = "pubkey1";
|
||||
const dTag = "dtag1";
|
||||
const relayUrl = "wss://relay1.com";
|
||||
const timestamp = 2000;
|
||||
|
||||
store.updateLastSeen(kind, pubkey, dTag, relayUrl, timestamp);
|
||||
|
||||
assert.strictEqual(store.getLastSeen(kind, pubkey, dTag, relayUrl), timestamp);
|
||||
|
||||
const saved = JSON.parse(localStorage.getItem(SYNC_METADATA_STORAGE_KEY));
|
||||
const key = `${kind}:${pubkey}:${dTag}:${relayUrl}`;
|
||||
const entry = saved.find(e => e[0] === key);
|
||||
assert.ok(entry);
|
||||
assert.strictEqual(entry[1], timestamp);
|
||||
});
|
||||
|
||||
test("should only update if timestamp is newer", () => {
|
||||
const store = new SyncMetadataStore();
|
||||
const kind = 1;
|
||||
const pubkey = "pubkey1";
|
||||
const dTag = "dtag1";
|
||||
const relayUrl = "wss://relay1.com";
|
||||
|
||||
store.updateLastSeen(kind, pubkey, dTag, relayUrl, 2000);
|
||||
assert.strictEqual(store.getLastSeen(kind, pubkey, dTag, relayUrl), 2000);
|
||||
|
||||
// Try older
|
||||
store.updateLastSeen(kind, pubkey, dTag, relayUrl, 1000);
|
||||
assert.strictEqual(store.getLastSeen(kind, pubkey, dTag, relayUrl), 2000);
|
||||
|
||||
// Try newer
|
||||
store.updateLastSeen(kind, pubkey, dTag, relayUrl, 3000);
|
||||
assert.strictEqual(store.getLastSeen(kind, pubkey, dTag, relayUrl), 3000);
|
||||
});
|
||||
|
||||
test("should get per-relay last seen", () => {
|
||||
const store = new SyncMetadataStore();
|
||||
const kind = 1;
|
||||
const pubkey = "pubkey1";
|
||||
const dTag = "dtag1";
|
||||
|
||||
store.updateLastSeen(kind, pubkey, dTag, "wss://relay1.com", 1000);
|
||||
store.updateLastSeen(kind, pubkey, dTag, "wss://relay2.com", 2000);
|
||||
store.updateLastSeen(kind, pubkey, "other", "wss://relay1.com", 3000); // Should be ignored
|
||||
|
||||
const result = store.getPerRelayLastSeen(kind, pubkey, dTag);
|
||||
|
||||
assert.deepStrictEqual(result, {
|
||||
"wss://relay1.com": 1000,
|
||||
"wss://relay2.com": 2000
|
||||
});
|
||||
});
|
||||
|
||||
test("should handle missing localStorage gracefully", () => {
|
||||
const originalLocalStorage = globalThis.localStorage;
|
||||
delete globalThis.localStorage; // Simulate missing localStorage
|
||||
|
||||
try {
|
||||
const store = new SyncMetadataStore();
|
||||
store.updateLastSeen(1, "p", "d", "r", 100); // Should not throw
|
||||
// Can't check persistence, but ensure no crash
|
||||
} finally {
|
||||
globalThis.localStorage = originalLocalStorage;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
import { test, describe, before, after, beforeEach, afterEach, mock } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import "../test-helpers/setup-localstorage.mjs";
|
||||
import * as attachmentService from "../../js/services/attachmentService.js";
|
||||
|
||||
// Mock URL.createObjectURL and revokeObjectURL
|
||||
const originalURL = globalThis.URL;
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
describe("attachmentService", () => {
|
||||
before(() => {
|
||||
if (!globalThis.URL) {
|
||||
globalThis.URL = {};
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
globalThis.URL.createObjectURL = mock.fn(() => "blob:mock-url");
|
||||
globalThis.URL.revokeObjectURL = mock.fn();
|
||||
globalThis.fetch = mock.fn();
|
||||
|
||||
// Clear cache before each test to ensure isolation,
|
||||
// but we need to be careful not to trigger revokeObjectURL calls that we count?
|
||||
// Actually, we should just clear it.
|
||||
attachmentService.clearAttachmentCache();
|
||||
// Then reset calls
|
||||
globalThis.URL.revokeObjectURL.mock.resetCalls();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (originalURL) {
|
||||
globalThis.URL = originalURL;
|
||||
} else {
|
||||
delete globalThis.URL;
|
||||
}
|
||||
if (originalFetch) {
|
||||
globalThis.fetch = originalFetch;
|
||||
} else {
|
||||
delete globalThis.fetch;
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.reset();
|
||||
attachmentService.clearAttachmentCache();
|
||||
});
|
||||
|
||||
describe("prepareAttachmentUpload", () => {
|
||||
test("should prepare upload without encryption", async () => {
|
||||
const file = new File(["test content"], "test.txt", { type: "text/plain" });
|
||||
const result = await attachmentService.prepareAttachmentUpload({ file, encrypt: false });
|
||||
|
||||
assert.strictEqual(result.name, "test.txt");
|
||||
assert.strictEqual(result.type, "text/plain");
|
||||
assert.strictEqual(result.size, 12);
|
||||
assert.strictEqual(result.encrypted, false);
|
||||
assert.strictEqual(result.key, "");
|
||||
// sha256 of "test content"
|
||||
assert.strictEqual(result.sha256, "6ae8a75555209fd6c44157c0aed8016e763ff435a19cf186f76863140143ff72");
|
||||
});
|
||||
|
||||
test("should prepare upload with encryption", async () => {
|
||||
const file = new File(["test content"], "test.txt", { type: "text/plain" });
|
||||
const result = await attachmentService.prepareAttachmentUpload({ file, encrypt: true });
|
||||
|
||||
assert.strictEqual(result.name, "test.txt");
|
||||
assert.strictEqual(result.type, "text/plain"); // Metadata preserves original type
|
||||
assert.strictEqual(result.encrypted, true);
|
||||
assert.ok(result.key.length > 0, "Should return a key");
|
||||
assert.notStrictEqual(result.uploadBlob, file, "Should return a new blob");
|
||||
assert.strictEqual(result.uploadBlob.type, "application/octet-stream"); // Actual blob is generic binary
|
||||
});
|
||||
});
|
||||
|
||||
describe("downloadAttachment", () => {
|
||||
test("should download attachment", async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
blob: async () => new Blob(["test content"]),
|
||||
headers: new Map(),
|
||||
};
|
||||
globalThis.fetch.mock.mockImplementation(async () => mockResponse);
|
||||
|
||||
const result = await attachmentService.downloadAttachment({ url: "https://example.com/file" });
|
||||
|
||||
assert.strictEqual(globalThis.fetch.mock.callCount(), 1);
|
||||
assert.strictEqual(result.objectUrl, "blob:mock-url");
|
||||
assert.strictEqual(result.size, 12);
|
||||
});
|
||||
|
||||
test("should verify hash", async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
blob: async () => new Blob(["test content"]),
|
||||
headers: new Map(),
|
||||
};
|
||||
globalThis.fetch.mock.mockImplementation(async () => mockResponse);
|
||||
|
||||
const expectedHash = "6ae8a75555209fd6c44157c0aed8016e763ff435a19cf186f76863140143ff72";
|
||||
await attachmentService.downloadAttachment({ url: "https://example.com/file", expectedHash });
|
||||
|
||||
// Should not throw
|
||||
});
|
||||
|
||||
test("should throw on hash mismatch", async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
blob: async () => new Blob(["test content"]),
|
||||
headers: new Map(),
|
||||
};
|
||||
globalThis.fetch.mock.mockImplementation(async () => mockResponse);
|
||||
|
||||
const expectedHash = "wronghash";
|
||||
|
||||
await assert.rejects(
|
||||
async () => {
|
||||
await attachmentService.downloadAttachment({ url: "https://example.com/file", expectedHash });
|
||||
},
|
||||
/Attachment hash mismatch/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("caching", () => {
|
||||
test("should cache downloaded attachments", async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
blob: async () => new Blob(["test content"]),
|
||||
headers: new Map(),
|
||||
};
|
||||
globalThis.fetch.mock.mockImplementation(async () => mockResponse);
|
||||
|
||||
const expectedHash = "6ae8a75555209fd6c44157c0aed8016e763ff435a19cf186f76863140143ff72";
|
||||
|
||||
// First download
|
||||
await attachmentService.downloadAttachment({ url: "https://example.com/file1", expectedHash });
|
||||
assert.strictEqual(globalThis.fetch.mock.callCount(), 1);
|
||||
|
||||
// Second download (should be cached)
|
||||
const result = await attachmentService.downloadAttachment({ url: "https://example.com/file1", expectedHash });
|
||||
// Fetch should NOT be called again
|
||||
assert.strictEqual(globalThis.fetch.mock.callCount(), 1);
|
||||
assert.strictEqual(result.cached, true);
|
||||
});
|
||||
|
||||
test("should clear cache", async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
blob: async () => new Blob(["test content"]),
|
||||
headers: new Map(),
|
||||
};
|
||||
globalThis.fetch.mock.mockImplementation(async () => mockResponse);
|
||||
const expectedHash = "6ae8a75555209fd6c44157c0aed8016e763ff435a19cf186f76863140143ff72";
|
||||
|
||||
await attachmentService.downloadAttachment({ url: "https://example.com/file", expectedHash });
|
||||
assert.strictEqual(attachmentService.getAttachmentCacheStats().size, 1);
|
||||
|
||||
// Clear call count for revokeObjectURL before clearing cache to isolate the count
|
||||
globalThis.URL.revokeObjectURL.mock.resetCalls();
|
||||
|
||||
attachmentService.clearAttachmentCache();
|
||||
assert.strictEqual(attachmentService.getAttachmentCacheStats().size, 0);
|
||||
assert.strictEqual(globalThis.URL.revokeObjectURL.mock.callCount(), 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { test, describe, afterEach } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { createUiDom } from "../ui/helpers/jsdom-test-helpers.mjs";
|
||||
|
||||
// Disable network imports to prevent S3 SDK loading
|
||||
globalThis.__BITVID_DISABLE_NETWORK_IMPORTS__ = true;
|
||||
|
||||
const s3Service = await import("../../js/services/s3Service.js");
|
||||
|
||||
describe("s3Service", () => {
|
||||
describe("validateS3Connection", () => {
|
||||
test("should validate correct configuration", () => {
|
||||
const config = {
|
||||
endpoint: "https://s3.example.com",
|
||||
region: "us-east-1",
|
||||
accessKeyId: "key",
|
||||
secretAccessKey: "secret",
|
||||
bucket: "my-bucket",
|
||||
forcePathStyle: true,
|
||||
};
|
||||
|
||||
const result = s3Service.validateS3Connection(config);
|
||||
assert.strictEqual(result.endpoint, "https://s3.example.com");
|
||||
assert.strictEqual(result.bucket, "my-bucket");
|
||||
assert.ok(result.publicBaseUrl.includes("my-bucket"));
|
||||
});
|
||||
|
||||
test("should throw on missing required fields", () => {
|
||||
assert.throws(() => s3Service.validateS3Connection({}), /S3 endpoint is required/);
|
||||
// Region defaults to auto, so next check is Access Key ID
|
||||
assert.throws(() => s3Service.validateS3Connection({ endpoint: "x" }), /S3 access key ID is required/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCorsOrigins", () => {
|
||||
let uiDom;
|
||||
const originalWindow = global.window;
|
||||
|
||||
afterEach(() => {
|
||||
if (uiDom) {
|
||||
uiDom.cleanup();
|
||||
uiDom = null;
|
||||
}
|
||||
global.window = originalWindow;
|
||||
});
|
||||
|
||||
test("should return current origin", () => {
|
||||
uiDom = createUiDom({ url: "https://app.bitvid.invalid/" });
|
||||
global.window = uiDom.window;
|
||||
|
||||
const origins = s3Service.getCorsOrigins();
|
||||
assert.ok(origins.includes("https://app.bitvid.invalid"));
|
||||
});
|
||||
|
||||
test("should handle localhost", () => {
|
||||
uiDom = createUiDom({ url: "http://localhost:3000/" });
|
||||
global.window = uiDom.window;
|
||||
|
||||
const origins = s3Service.getCorsOrigins();
|
||||
assert.ok(origins.includes("http://localhost:3000"));
|
||||
assert.ok(origins.includes("https://localhost:3000")); // Should add https version
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -45,6 +45,11 @@ test.describe("overlay layering tokens", () => {
|
||||
|
||||
await dismissDisclaimerModal(page);
|
||||
|
||||
// Wait for initial fade-in to complete so opacity doesn't interfere with visibility checks
|
||||
await page.waitForFunction(
|
||||
() => !document.getElementById("sidebar")?.classList.contains("fade-in")
|
||||
);
|
||||
|
||||
await expect(page.locator("#mobileMenuBtn")).toHaveCount(0);
|
||||
|
||||
const collapseToggle = page.locator("#sidebarCollapseToggle");
|
||||
@@ -123,14 +128,26 @@ test.describe("overlay layering tokens", () => {
|
||||
|
||||
// Ensure the toggle is initialized
|
||||
await expect(collapseToggle).toHaveAttribute("data-state", /.*/);
|
||||
await collapseToggle.click();
|
||||
await collapseToggle.click({ force: true });
|
||||
|
||||
await page.waitForFunction(() =>
|
||||
document.getElementById("sidebar")?.classList.contains("sidebar-expanded")
|
||||
);
|
||||
await page.waitForFunction(() => {
|
||||
const sidebar = document.getElementById("sidebar");
|
||||
return (
|
||||
sidebar?.classList.contains("sidebar-expanded") &&
|
||||
!sidebar?.classList.contains("sidebar-collapsed")
|
||||
);
|
||||
});
|
||||
|
||||
// Wait for sidebar transition to complete
|
||||
await page.waitForTimeout(500);
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Ensure the footer button is scrolled into view (especially for mobile viewports)
|
||||
await page.evaluate(() => {
|
||||
const sidebar = document.getElementById("sidebar");
|
||||
if (sidebar) {
|
||||
sidebar.scrollTop = sidebar.scrollHeight;
|
||||
}
|
||||
});
|
||||
|
||||
const expandedLayout = await page.evaluate(() => {
|
||||
const sidebar = document.getElementById("sidebar");
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
|
||||
import { subscriptions } from './js/subscriptions.js';
|
||||
import { hashtagPreferences } from './js/services/hashtagPreferencesService.js';
|
||||
import { profileCache } from './js/state/profileCache.js';
|
||||
|
||||
// Mock profileCache.getProfileData
|
||||
const originalGetProfileData = profileCache.getProfileData;
|
||||
const originalGet = profileCache.get;
|
||||
|
||||
let calls = [];
|
||||
|
||||
profileCache.getProfileData = (pubkey, section) => {
|
||||
calls.push({ method: 'getProfileData', pubkey, section });
|
||||
return null;
|
||||
};
|
||||
|
||||
profileCache.get = (section) => {
|
||||
calls.push({ method: 'get', section });
|
||||
return null;
|
||||
};
|
||||
|
||||
async function runVerify() {
|
||||
console.log("Verifying fix...");
|
||||
|
||||
// Test SubscriptionsManager
|
||||
const subPubkey = "0000000000000000000000000000000000000000000000000000000000000001";
|
||||
// We need to mock updateFromRelays to avoid network calls, or just catch error
|
||||
subscriptions.updateFromRelays = async () => {};
|
||||
|
||||
await subscriptions.loadSubscriptions(subPubkey);
|
||||
|
||||
const subCall = calls.find(c => c.section === "subscriptions");
|
||||
if (subCall && subCall.method === "getProfileData" && subCall.pubkey === subPubkey) {
|
||||
console.log("PASS: subscriptions.loadSubscriptions used getProfileData with correct pubkey.");
|
||||
} else {
|
||||
console.error("FAIL: subscriptions.loadSubscriptions did not use getProfileData correctly.", calls);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
calls = []; // Reset
|
||||
|
||||
// Test HashtagPreferencesService
|
||||
const tagPubkey = "0000000000000000000000000000000000000000000000000000000000000002";
|
||||
// Mock load logic to avoid network
|
||||
hashtagPreferences.loadFromCache(tagPubkey);
|
||||
|
||||
const tagCall = calls.find(c => c.section === "interests");
|
||||
if (tagCall && tagCall.method === "getProfileData" && tagCall.pubkey === tagPubkey) {
|
||||
console.log("PASS: hashtagPreferences.loadFromCache used getProfileData with correct pubkey.");
|
||||
} else {
|
||||
console.error("FAIL: hashtagPreferences.loadFromCache did not use getProfileData correctly.", calls);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("All verifications passed.");
|
||||
}
|
||||
|
||||
runVerify().catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,91 +0,0 @@
|
||||
from playwright.sync_api import sync_playwright
|
||||
import time
|
||||
|
||||
def verify_source_toggle():
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
page = browser.new_page()
|
||||
|
||||
# Navigate to the local server
|
||||
page.goto("http://localhost:8080/index.html")
|
||||
|
||||
# Wait for the app to initialize (give it a moment)
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
# We need to simulate opening the modal with a video that has both sources
|
||||
# We'll use page.evaluate to interact with the Javascript context
|
||||
# Assuming 'app' is globally available or we can instantiate VideoModal
|
||||
|
||||
page.evaluate("""
|
||||
async () => {
|
||||
// Mock a video object with both URL and Magnet
|
||||
const mockVideo = {
|
||||
id: 'test-video-123',
|
||||
title: 'Test Video with Source Toggle',
|
||||
url: 'https://example.com/video.mp4',
|
||||
magnet: 'magnet:?xt=urn:btih:example',
|
||||
torrentSupported: true,
|
||||
moderation: {}
|
||||
};
|
||||
|
||||
// Access the global app instance or find the video modal
|
||||
// Looking at js/app.js, it might attach to window.app
|
||||
// If not, we might need to find the modal instance another way.
|
||||
|
||||
// Let's assume window.app exists as is common in these setups,
|
||||
// or we try to find the modal controller.
|
||||
|
||||
if (window.app && window.app.videoModal) {
|
||||
await window.app.videoModal.open(mockVideo);
|
||||
} else {
|
||||
console.error("Could not find window.app.videoModal");
|
||||
// Fallback: Try to find the modal element and manually unhide it
|
||||
// to verify the toggle structure exists, though open() is better.
|
||||
|
||||
// Note: If window.app isn't exposed, we might struggle to invoke .open() directly
|
||||
// without module injection.
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
# Wait for the modal to appear
|
||||
try:
|
||||
page.wait_for_selector("#playerModal", state="visible", timeout=5000)
|
||||
print("Modal opened successfully.")
|
||||
except:
|
||||
print("Modal did not open via app.videoModal. attempting manual check...")
|
||||
|
||||
# specific check for the toggle container
|
||||
# The toggle container has attribute [data-source-toggle-container]
|
||||
toggle_selector = "[data-source-toggle-container]"
|
||||
|
||||
# Expect it to be visible
|
||||
try:
|
||||
page.wait_for_selector(toggle_selector, state="visible", timeout=2000)
|
||||
print("Source toggle container is visible!")
|
||||
except:
|
||||
print("Source toggle container NOT visible or not found.")
|
||||
# If it's not visible, maybe the video didn't load right.
|
||||
# Let's try to inspect the DOM to see if the element exists at all.
|
||||
content = page.content()
|
||||
if "data-source-toggle-container" in content:
|
||||
print("Element exists in DOM but might be hidden.")
|
||||
else:
|
||||
print("Element does NOT exist in DOM.")
|
||||
|
||||
# Take a screenshot of the modal
|
||||
# We can focus on the stats container where the toggle is
|
||||
stats_selector = "[data-video-stats-container]"
|
||||
try:
|
||||
page.wait_for_selector(stats_selector, state="visible")
|
||||
# Screenshot the whole page or just the modal
|
||||
page.screenshot(path="/home/jules/verification/verification.png")
|
||||
print("Screenshot saved.")
|
||||
except Exception as e:
|
||||
print(f"Could not take targeted screenshot: {e}")
|
||||
page.screenshot(path="/home/jules/verification/verification_fallback.png")
|
||||
|
||||
browser.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
verify_source_toggle()
|
||||
Reference in new issue
Block a user