mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 09:19:42 +02:00
Merge #551: remove the broken tunnel upload page and deprecate /api/screenshots
This commit is contained in:
@@ -24,7 +24,6 @@ src/web/public/settings-ui.js
|
||||
src/web/public/sw.js
|
||||
src/web/public/terminal-ui.js
|
||||
src/web/public/voice-input.js
|
||||
src/web/public/upload.html
|
||||
scripts/remotion/
|
||||
|
||||
# Hand-maintained; Prettier escapes underscores in glob paths and corrupts paragraphs.
|
||||
|
||||
@@ -127,7 +127,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph
|
||||
|
||||
**Code style**: Prettier (`singleQuote: true`, `printWidth: 120`, `trailingComma: "es5"`) — config lives in the **`"prettier"` key of `package.json`**, not a `.prettierrc` (keeps the repo root short; editors read it natively). `.prettierignore` stays at the root because Prettier resolves it relative to cwd. ESLint flat config (`config/eslint.config.js`) allows `no-console`, warns on `@typescript-eslint/no-explicit-any`. Ignores: `app.js`, `scripts/**/*.mjs`, `src/web/public/vendor/**`, `scripts/remotion/**`.
|
||||
|
||||
**Prettier scope is deliberately narrow.** `npm run format` globs only `src/**/*.ts` and `src/web/public/**` (`lint` only `src/**/*.ts`), and `.prettierignore` then exempts most of `src/web/public/*.js` (app.js, styles.css, **mobile.css**, index.html, upload.html, and 15 hand-formatted modules) plus `CLAUDE.md`. Those files are hand-formatted by design; `npm run check:public-assets` and `check:frontend-syntax` are what guard them (NUL bytes + JS syntax), not Prettier. Do not "fix" a file by adding it back to Prettier's scope.
|
||||
**Prettier scope is deliberately narrow.** `npm run format` globs only `src/**/*.ts` and `src/web/public/**` (`lint` only `src/**/*.ts`), and `.prettierignore` then exempts most of `src/web/public/*.js` (app.js, styles.css, **mobile.css**, index.html, and 15 hand-formatted modules) plus `CLAUDE.md`. Those files are hand-formatted by design; `npm run check:public-assets` and `check:frontend-syntax` are what guard them (NUL bytes + JS syntax), not Prettier. Do not "fix" a file by adding it back to Prettier's scope.
|
||||
|
||||
## Common Gotchas
|
||||
|
||||
@@ -493,7 +493,7 @@ curl -sk https://localhost:3000/api/subagents | jq # Background agents
|
||||
cat ~/.codeman/state.json | jq # Persisted state
|
||||
```
|
||||
|
||||
Mobile screenshots: `~/.codeman/screenshots/`, accessed via `GET/POST /api/screenshots`.
|
||||
Legacy screenshots (deprecated): `GET/POST /api/screenshots` still read and write `~/.codeman/screenshots/`, but the upload page that fed them is gone, they log a one-time deprecation warning, and they are removed in a later MAJOR, after at least one MINOR release that carries the warning (`docs/versioning-policy.md`). To hand a file to an agent use `POST /api/sessions/:id/paste-image`.
|
||||
|
||||
## Performance & Limits
|
||||
|
||||
|
||||
@@ -45,6 +45,13 @@ payload return `{ "success": true, "data": {} }`.
|
||||
> `GET /api/sessions/:id/tail-file` (SSE), `GET /api/download`,
|
||||
> `GET /api/screenshots/:name`, `GET /q/:code` (QR redirect), and the
|
||||
> `GET /ws/sessions/:id/terminal` WebSocket upgrade.
|
||||
>
|
||||
> **Deprecated:** `POST /api/screenshots`, `GET /api/screenshots` and
|
||||
> `GET /api/screenshots/:name` keep working but log a one-time warning on first
|
||||
> use. They are removed in a later MAJOR, after at least one MINOR release that
|
||||
> carries this warning (see `docs/versioning-policy.md`). To hand
|
||||
> a file to an agent, use `POST /api/sessions/:id/paste-image`, which saves it into
|
||||
> that session's workspace.
|
||||
|
||||
> The [agent wait endpoints](#long-polling-agent-wait) use the normal envelope but
|
||||
> are the only JSON endpoints that deliberately **hold the connection open**, for up
|
||||
|
||||
@@ -158,7 +158,7 @@ Codeman now ships a global **Startup Mode** picker (App Settings, Claude CLI tab
|
||||
| `GET /api/away-digest` | Aggregate only owned sessions/events |
|
||||
| `GET /api/subagents`, workflow runs | Filter by owning session (`claudeSessionId -> session -> owner`); agents not attributable to any session: admin-only |
|
||||
| Push (`push-routes.ts`) | Subscription records currently carry NO identity (keyed by endpoint only): `subscribe` stamps `username`. All 8 `PUSH_EVENT_MAP` events are session-scoped, so routing = resolve owner from `data.sessionId`, deliver to that owner's (plus admins') subscriptions. Legacy identity-less subscriptions: admin-only delivery |
|
||||
| Screenshots `/api/screenshots` | Per-user subdir `~/.codeman/screenshots/<username>/` in multi-user mode. Note: `GET /:name` deliberately rejects `/` in names as traversal, so derive the subdir server-side from `req.authUser` and keep client-visible names flat |
|
||||
| Screenshots `/api/screenshots` (deprecated) | Deprecated: removed in a later MAJOR, so no per-user subdir is planned; the replacement `POST /api/sessions/:id/paste-image` is already session-scoped. Former plan: per-user subdir `~/.codeman/screenshots/<username>/` in multi-user mode. Note: `GET /:name` deliberately rejects `/` in names as traversal, so derive the subdir server-side from `req.authUser` and keep client-visible names flat |
|
||||
| Attachments | Already session-scoped; inherits the session owner check. `attachmentConfineToWorkspace` is a global, default-OFF setting today: in multi-user mode it is FORCED ON for non-admins regardless of the setting (their attachments must resolve inside their own space); the setting keeps meaning what it means for admins |
|
||||
| File routes (browse/preview) | Path allowlist adds: non-admin paths must resolve (realpath) inside their own space or their own sessions' workingDirs |
|
||||
| Settings (`settings.json`) | Global, admin-only writes in multi-user mode; reads allowed (per-device display keys stay in localStorage as today). Per-user server settings: out of scope v1 |
|
||||
@@ -228,7 +228,7 @@ These operate directly on `users.json` via `user-store.ts` (no server needed), h
|
||||
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Default (no flag) | No behavior change. No new file reads on the hot path. All new fields optional in state |
|
||||
| State round-trip | `SessionState.owner`, `MuxSession.owner`, `CronJob.owner`, registry `owner` fields are optional; old state loads clean; new state loaded by an old build ignores unknown fields (existing tolerant parsing) |
|
||||
| Instance isolation | `users.json`, audit log, screenshots subdirs all via `dataPath()`; user spaces dir is shared across instances like `~/codeman-cases` is today (documented) |
|
||||
| Instance isolation | `users.json` and the audit log via `dataPath()`; user spaces dir is shared across instances like `~/codeman-cases` is today (documented) |
|
||||
| API versioning | HTTP API is internal per `docs/versioning-policy.md`; still, all changes are additive. Ship as a **minor** version |
|
||||
| Hooks | Unchanged (instance-level hook secret; owner resolved from the session) |
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ Rebinding for the shortcut registry. See [Keyboard Shortcuts](Keyboard-Shortcuts
|
||||
### System
|
||||
|
||||
`CLAUDE.md` template for new cases, default working directory, the image watcher, and
|
||||
Cloudflare tunnel controls including the tunnel and upload URLs. The **Diagnostics** group runs
|
||||
Cloudflare tunnel controls including the tunnel URL. The **Diagnostics** group runs
|
||||
`codeman doctor` on the server and lists the agent CLIs, tmux, Node and the optional office
|
||||
tools with their versions and install hints (admin only in multi-user mode). In multi-user
|
||||
mode, the **Users** administration entry is injected here.
|
||||
|
||||
@@ -448,7 +448,6 @@
|
||||
'Remote Access': '远程访问',
|
||||
'Cloudflare Tunnel': 'Cloudflare 隧道',
|
||||
'Tunnel URL': '隧道地址',
|
||||
'Upload URL': '上传地址',
|
||||
Updates: '更新',
|
||||
'Current Version': '当前版本',
|
||||
'Check for Updates': '检查更新',
|
||||
|
||||
@@ -2992,10 +2992,6 @@
|
||||
<button class="btn-icon-sm" id="tunnelQrBtn" onclick="app.showTunnelQR()" title="Show QR code">⊞</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="set-row" id="tunnelUploadUrlRow" style="display:none">
|
||||
<div class="set-row-text"><span class="set-row-label">Upload URL</span></div>
|
||||
<span id="tunnelUploadUrlDisplay" class="set-copy" title="Click to copy"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1760,17 +1760,16 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
},
|
||||
|
||||
_updateTunnelUrlRow(rowId, displayId, url, suffix = '') {
|
||||
const row = document.getElementById(rowId);
|
||||
const display = document.getElementById(displayId);
|
||||
_updateTunnelUrlDisplay(url) {
|
||||
const row = document.getElementById('tunnelUrlRow');
|
||||
const display = document.getElementById('tunnelUrlDisplay');
|
||||
if (!row || !display) return;
|
||||
if (url) {
|
||||
const fullUrl = url + suffix;
|
||||
row.style.display = '';
|
||||
display.textContent = fullUrl;
|
||||
display.textContent = url;
|
||||
display.onclick = () => {
|
||||
navigator.clipboard.writeText(fullUrl).then(() => {
|
||||
this.showToast(`${suffix ? 'Upload' : 'Tunnel'} URL copied`, 'success');
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
this.showToast('Tunnel URL copied', 'success');
|
||||
});
|
||||
};
|
||||
} else {
|
||||
@@ -1780,11 +1779,6 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
},
|
||||
|
||||
_updateTunnelUrlDisplay(url) {
|
||||
this._updateTunnelUrlRow('tunnelUrlRow', 'tunnelUrlDisplay', url);
|
||||
this._updateTunnelUrlRow('tunnelUploadUrlRow', 'tunnelUploadUrlDisplay', url, '/upload.html');
|
||||
},
|
||||
|
||||
showTunnelQR() {
|
||||
// Close existing popup if open
|
||||
this.closeTunnelQR();
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"><head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||
<title>Upload Screenshot - Codeman</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
background: #0d1117; color: #c9d1d9;
|
||||
min-height: 100vh; min-height: 100dvh;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
.container { max-width: 480px; width: 100%; padding: 24px; }
|
||||
h1 { font-size: 1.4em; margin-bottom: 16px; color: #58a6ff; }
|
||||
.drop-zone {
|
||||
border: 2px dashed #30363d; border-radius: 12px;
|
||||
padding: 48px 20px; text-align: center;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
}
|
||||
.drop-zone.active { border-color: #58a6ff; background: #161b22; }
|
||||
.drop-zone p { margin-bottom: 12px; font-size: 1.1em; }
|
||||
.drop-zone small { color: #8b949e; }
|
||||
input[type=file] { display: none; }
|
||||
.preview { margin-top: 16px; text-align: center; display: none; }
|
||||
.preview img { max-width: 100%; max-height: 300px; border-radius: 8px; border: 1px solid #30363d; }
|
||||
.preview .name { margin-top: 6px; font-size: 0.85em; color: #8b949e; word-break: break-all; }
|
||||
button {
|
||||
width: 100%; padding: 14px; margin-top: 16px;
|
||||
background: #238636; color: #fff; border: none;
|
||||
border-radius: 8px; font-size: 1.05em;
|
||||
font-weight: 600; -webkit-appearance: none;
|
||||
opacity: 0.4; pointer-events: none;
|
||||
}
|
||||
button.ready { opacity: 1; pointer-events: auto; }
|
||||
button:active { background: #2ea043; }
|
||||
.status { margin-top: 12px; padding: 10px; border-radius: 6px; text-align: center; display: none; }
|
||||
.status.ok { display: block; background: #1a3a2a; color: #3fb950; }
|
||||
.status.err { display: block; background: #3a1a1a; color: #f85149; }
|
||||
.files { margin-top: 24px; }
|
||||
.files h2 { font-size: 0.95em; color: #8b949e; margin-bottom: 8px; }
|
||||
.files a { display: block; color: #58a6ff; text-decoration: none; padding: 6px 0; font-size: 0.9em; word-break: break-all; }
|
||||
.files a:active { color: #79c0ff; }
|
||||
.back { display: inline-block; margin-bottom: 12px; color: #8b949e; text-decoration: none; font-size: 0.9em; }
|
||||
.back:active { color: #c9d1d9; }
|
||||
</style>
|
||||
</head><body>
|
||||
<div class="container">
|
||||
<a class="back" href="/">← Back to Codeman</a>
|
||||
<h1>Upload Screenshot</h1>
|
||||
<div class="drop-zone" id="drop">
|
||||
<p>Tap to select image</p>
|
||||
<small>PNG, JPG, WebP — up to 10 MB</small>
|
||||
</div>
|
||||
<input type="file" id="file" accept="image/*">
|
||||
<div class="preview" id="preview">
|
||||
<img id="previewImg" alt="Preview">
|
||||
<div class="name" id="previewName"></div>
|
||||
</div>
|
||||
<button id="btn">Upload</button>
|
||||
<div class="status" id="status"></div>
|
||||
<div class="files" id="files"></div>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
var drop = document.getElementById('drop');
|
||||
var fileInput = document.getElementById('file');
|
||||
var preview = document.getElementById('preview');
|
||||
var previewImg = document.getElementById('previewImg');
|
||||
var previewName = document.getElementById('previewName');
|
||||
var btn = document.getElementById('btn');
|
||||
var status = document.getElementById('status');
|
||||
var filesDiv = document.getElementById('files');
|
||||
var selectedFile = null;
|
||||
|
||||
drop.addEventListener('click', function() { fileInput.click(); });
|
||||
|
||||
fileInput.addEventListener('change', function() {
|
||||
if (fileInput.files && fileInput.files[0]) pick(fileInput.files[0]);
|
||||
});
|
||||
|
||||
// Drag-and-drop (desktop fallback)
|
||||
drop.addEventListener('dragover', function(e) { e.preventDefault(); drop.classList.add('active'); });
|
||||
drop.addEventListener('dragleave', function() { drop.classList.remove('active'); });
|
||||
drop.addEventListener('drop', function(e) {
|
||||
e.preventDefault(); drop.classList.remove('active');
|
||||
if (e.dataTransfer && e.dataTransfer.files[0]) pick(e.dataTransfer.files[0]);
|
||||
});
|
||||
|
||||
function pick(f) {
|
||||
selectedFile = f;
|
||||
previewImg.src = URL.createObjectURL(f);
|
||||
previewName.textContent = f.name + ' (' + (f.size / 1024).toFixed(0) + ' KB)';
|
||||
preview.style.display = 'block';
|
||||
btn.classList.add('ready');
|
||||
status.className = 'status';
|
||||
status.style.display = 'none';
|
||||
}
|
||||
|
||||
btn.addEventListener('click', function() {
|
||||
if (!selectedFile) return;
|
||||
btn.classList.remove('ready');
|
||||
btn.textContent = 'Uploading\u2026';
|
||||
var form = new FormData();
|
||||
form.append('file', selectedFile);
|
||||
fetch('/api/screenshots', { method: 'POST', body: form })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(j) {
|
||||
if (j.success) {
|
||||
status.className = 'status ok';
|
||||
status.textContent = 'Saved: ' + j.filename;
|
||||
status.style.display = 'block';
|
||||
selectedFile = null;
|
||||
preview.style.display = 'none';
|
||||
btn.textContent = 'Upload';
|
||||
loadFiles();
|
||||
} else {
|
||||
status.className = 'status err';
|
||||
status.textContent = j.error || 'Upload failed';
|
||||
status.style.display = 'block';
|
||||
btn.classList.add('ready');
|
||||
btn.textContent = 'Upload';
|
||||
}
|
||||
})
|
||||
.catch(function(e) {
|
||||
status.className = 'status err';
|
||||
status.textContent = e.message;
|
||||
status.style.display = 'block';
|
||||
btn.classList.add('ready');
|
||||
btn.textContent = 'Upload';
|
||||
});
|
||||
});
|
||||
|
||||
function loadFiles() {
|
||||
fetch('/api/screenshots')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(j) {
|
||||
if (j.files && j.files.length) {
|
||||
var html = '<h2>Recent uploads</h2>';
|
||||
j.files.forEach(function(f) {
|
||||
html += '<a href="/api/screenshots/' + encodeURIComponent(f.name) + '" target="_blank">' + f.name + '</a>';
|
||||
});
|
||||
filesDiv.innerHTML = html;
|
||||
}
|
||||
})
|
||||
.catch(function() {});
|
||||
}
|
||||
|
||||
loadFiles();
|
||||
})();
|
||||
</script>
|
||||
</body></html>
|
||||
@@ -1295,8 +1295,20 @@ export function registerSystemRoutes(
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
// ========== Screenshots ==========
|
||||
// Deprecated (the upload page is gone): removed in a later MAJOR per
|
||||
// docs/versioning-policy.md. Warns once per process on first use.
|
||||
let screenshotsDeprecationWarned = false;
|
||||
const warnScreenshotsDeprecated = (): void => {
|
||||
if (screenshotsDeprecationWarned) return;
|
||||
screenshotsDeprecationWarned = true;
|
||||
console.warn(
|
||||
'[deprecated] /api/screenshots is deprecated and will be removed in a future major release. ' +
|
||||
'Use POST /api/sessions/:id/paste-image to hand a file to a session.'
|
||||
);
|
||||
};
|
||||
|
||||
app.post('/api/screenshots', async (req, reply) => {
|
||||
warnScreenshotsDeprecated();
|
||||
const contentType = req.headers['content-type'] ?? '';
|
||||
if (!contentType.includes('multipart/form-data')) {
|
||||
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Expected multipart/form-data');
|
||||
@@ -1383,6 +1395,7 @@ export function registerSystemRoutes(
|
||||
});
|
||||
|
||||
app.get('/api/screenshots', async () => {
|
||||
warnScreenshotsDeprecated();
|
||||
if (!existsSync(SCREENSHOTS_DIR)) {
|
||||
return { files: [] };
|
||||
}
|
||||
@@ -1396,6 +1409,7 @@ export function registerSystemRoutes(
|
||||
});
|
||||
|
||||
app.get('/api/screenshots/:name', async (req, reply) => {
|
||||
warnScreenshotsDeprecated();
|
||||
const { name } = req.params as { name: string };
|
||||
// Prevent path traversal
|
||||
if (name.includes('/') || name.includes('\\') || name.includes('..')) {
|
||||
|
||||
+5
-8
@@ -932,7 +932,9 @@ export class WebServer extends EventEmitter {
|
||||
});
|
||||
|
||||
// Serve static files — content-hashed assets (e.g. app.a3f8c2e1.js) are immutable, cache aggressively.
|
||||
// HTML must revalidate every time so browsers pick up new hashed filenames after deploys.
|
||||
// HTML must revalidate every time so browsers pick up new hashed filenames after deploys, and every
|
||||
// HTML page has its own route that says so (/, /index.html, /session/:id). ⚠️ A new static .html
|
||||
// needs such a route too: this plugin would hand it a year of `immutable`.
|
||||
// cacheControl disabled so setHeaders owns Cache-Control for plain static assets.
|
||||
// preCompressed: serve pre-built .br/.gz files (from build step) to avoid per-request CPU compression
|
||||
await this.app.register(fastifyStatic, {
|
||||
@@ -944,7 +946,7 @@ export class WebServer extends EventEmitter {
|
||||
// `ServerResponse` to a `FastifyReply`, so it is `reply.header()` here and
|
||||
// NOT `res.setHeader()`. A v9-style body throws TypeError on every static
|
||||
// request, which is every page load. See the v10.0.0 release notes.
|
||||
setHeaders: (reply, path) => {
|
||||
setHeaders: (reply) => {
|
||||
// ⚠️ That same change ALSO flipped precedence, and silently. Under v9 this
|
||||
// callback wrote to the raw response and Fastify's staged reply headers then
|
||||
// overwrote it, so a route that set its own Cache-Control before .sendFile()
|
||||
@@ -953,12 +955,7 @@ export class WebServer extends EventEmitter {
|
||||
// no-store` its route asks for — a service worker that can never update.
|
||||
// So: a route that already decided keeps its answer.
|
||||
if (reply.getHeader('Cache-Control') !== undefined) return;
|
||||
// Use .includes() not .endsWith() — preCompressed serves .html.br/.html.gz
|
||||
if (path.includes('.html')) {
|
||||
reply.header('Cache-Control', 'no-cache');
|
||||
} else {
|
||||
reply.header('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
}
|
||||
reply.header('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -680,6 +680,24 @@ describe('system-routes', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('/api/screenshots deprecation', () => {
|
||||
it('warns once across requests and leaves the response unchanged', async () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
try {
|
||||
mockedExistsSync.mockReturnValue(false);
|
||||
const first = await harness.app.inject({ method: 'GET', url: '/api/screenshots' });
|
||||
const second = await harness.app.inject({ method: 'GET', url: '/api/screenshots' });
|
||||
expect(JSON.parse(first.body)).toEqual({ files: [] });
|
||||
expect(JSON.parse(second.body)).toEqual({ files: [] });
|
||||
const deprecations = warn.mock.calls.filter((c) => String(c[0]).includes('/api/screenshots'));
|
||||
expect(deprecations).toHaveLength(1);
|
||||
expect(String(deprecations[0][0])).toContain('POST /api/sessions/:id/paste-image');
|
||||
} finally {
|
||||
warn.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ========== GET /api/screenshots/:name ==========
|
||||
|
||||
describe('GET /api/screenshots/:name', () => {
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
*
|
||||
* That contract is load-bearing: assets are served `immutable` for a year, and
|
||||
* `index.html` must revalidate every time or a deploy leaves browsers on stale
|
||||
* markup (see `cacheBustAssets` in server.ts).
|
||||
* markup (see `cacheBustAssets` in server.ts). No HTML is left behind the static
|
||||
* plugin (every page has its own route), so the HTML half is asserted on the route.
|
||||
*
|
||||
* These tests drive a REAL WebServer on purpose. Asserting against an inline
|
||||
* re-registration of the plugin would keep passing after a revert in server.ts.
|
||||
@@ -61,18 +62,6 @@ describe('static asset Cache-Control headers', () => {
|
||||
expect(res.headers.get('cache-control')).toBe('public, max-age=31536000, immutable');
|
||||
});
|
||||
|
||||
it('makes static HTML revalidate so deploys are picked up', async () => {
|
||||
// ⚠️ upload.html, NOT index.html. `/index.html` has its own explicit route that
|
||||
// answers from renderIndexHtml() and never reaches @fastify/static, so asserting
|
||||
// on it passes even with setHeaders fully broken (verified: reverting server.ts
|
||||
// to the v9 form fails the two /app.js tests and leaves an index.html assertion
|
||||
// green). upload.html has no route of its own, so it is the only HTML that
|
||||
// actually exercises the `.html` branch of setHeaders.
|
||||
const res = await get(`${baseUrl}/upload.html`);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('cache-control')).toBe('no-cache');
|
||||
});
|
||||
|
||||
it('lets a route keep the Cache-Control it set, so sw.js stays uncached', async () => {
|
||||
// Regression guard for the OTHER half of the v10 change. setHeaders runs for
|
||||
// sendFile() too, and v10 moved it from the raw response onto the reply, which
|
||||
|
||||
Reference in New Issue
Block a user