fix(scripts): only publish images the repository owns, and keep their suffix
Review found four real gaps in the image placement this PR introduced. Link rewriting only needs a target to exist, but publication copies its bytes onto the site: a reference reaching out of the tree through `../..` or a symlink would put a build-machine file on a published page. Only a regular file whose real path stays inside the repository is copied now, and anything else fails the projection naming the page and the target. A placed reference kept none of its `?query` or `#fragment`, which the GitHub branch has always carried and which decides what an SVG view fragment or a Vite query means. The suffix rides along again, and the file name is percent-encoded because the destination is a Markdown inline target. Page outputs and placed images now claim projected paths from one map, so the "fail loud rather than overwrite" invariant covers a page and an image landing on one path, not only two images. `docsSourceFiles()` reports placed images, so replacing a screenshot re-projects under `docs:dev` instead of serving the previous copy until something touches the page. The guide said to set `agent-loop`'s `agents` to change the default model, which does nothing for `dsh web`: that default is `api-gateway`'s, and the shipped composition leaves `agents` empty. It also promised that a catalog provider needs only an API key, which is false for Bedrock, Vertex, Azure, and Codex. Both are corrected. The projection note and the doc-site skill carried the superseded "a repository image becomes a raw GitHub URL" rule; both now describe what ships.
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
/** Tests for the documentation website projection adapter. */
|
||||
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { docsPages, type DocsPage } from '../website/docs.ts'
|
||||
import { addProjectionFrontmatter, projectedPageContent, rewriteMarkdown } from './project-doc-site.ts'
|
||||
import {
|
||||
addProjectionFrontmatter, projectedPageContent, publishableImage, rewriteMarkdown,
|
||||
} from './project-doc-site.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
const repositoryRoot = resolve(import.meta.dirname, '..')
|
||||
@@ -63,6 +65,32 @@ describe('website source layout', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('publishableImage', () => {
|
||||
it('accepts a regular file inside the repository', () => {
|
||||
const { root } = fixture()
|
||||
const real = realpathSync(join(root, 'packages/logo.svg'))
|
||||
expect(publishableImage(join(root, 'packages/logo.svg'), realpathSync(root))).toBe(real)
|
||||
})
|
||||
|
||||
it('refuses a target whose real path escapes the repository', () => {
|
||||
// Publication copies the bytes onto the site, so a reference reaching a
|
||||
// build-machine file must not be treated as an image the repository owns.
|
||||
const { root } = fixture()
|
||||
const outside = mkdtempSync(join(tmpdir(), 'dsh-doc-site-outside-'))
|
||||
roots.push(outside)
|
||||
writeFileSync(join(outside, 'secret.png'), 'not really a png\n')
|
||||
symlinkSync(join(outside, 'secret.png'), join(root, 'packages/linked.png'))
|
||||
|
||||
expect(publishableImage(join(root, 'packages/linked.png'), realpathSync(root))).toBeUndefined()
|
||||
expect(publishableImage(join(outside, 'secret.png'), realpathSync(root))).toBeUndefined()
|
||||
})
|
||||
|
||||
it('refuses a directory', () => {
|
||||
const { root } = fixture()
|
||||
expect(publishableImage(join(root, 'packages'), realpathSync(root))).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('rewriteMarkdown', () => {
|
||||
it('maps published pages and pins unpublished source links', () => {
|
||||
const { root, pages } = fixture()
|
||||
@@ -107,7 +135,9 @@ describe('rewriteMarkdown', () => {
|
||||
|
||||
it('hands an image to the placer and uses the URL it returns', () => {
|
||||
// A raw GitHub URL cannot serve a private repository, so the site build
|
||||
// carries images itself; the placer is what puts them there.
|
||||
// carries images itself; the placer is what puts them there. The stand-in
|
||||
// derives its URL the way the real one does, so a placer that stopped
|
||||
// returning the basename would fail here rather than pass on a constant.
|
||||
const { root, pages } = fixture()
|
||||
const placed: string[] = []
|
||||
expect(rewriteMarkdown('\n', {
|
||||
@@ -118,13 +148,29 @@ describe('rewriteMarkdown', () => {
|
||||
repoRoot: root,
|
||||
repositoryRef: 'abc123',
|
||||
placeImage: (absPath) => {
|
||||
placed.push(absPath.split('/').pop() ?? '')
|
||||
return './logo.svg'
|
||||
const name = absPath.split('/').pop() ?? ''
|
||||
placed.push(name)
|
||||
return `./${name}`
|
||||
},
|
||||
})).toBe('\n')
|
||||
expect(placed).toEqual(['logo.svg'])
|
||||
})
|
||||
|
||||
it('keeps a placed image\u2019s query or fragment', () => {
|
||||
// An SVG view fragment and a Vite query both change what the reference
|
||||
// means, and the GitHub branch has always carried them.
|
||||
const { root, pages } = fixture()
|
||||
expect(rewriteMarkdown('\n', {
|
||||
locale: 'en',
|
||||
sourcePath: 'docs/a.md',
|
||||
route: 'en/a.md',
|
||||
pages,
|
||||
repoRoot: root,
|
||||
repositoryRef: 'abc123',
|
||||
placeImage: absPath => `./${absPath.split('/').pop() ?? ''}`,
|
||||
})).toBe('\n')
|
||||
})
|
||||
|
||||
it('leaves a published page link to the route even when a placer exists', () => {
|
||||
const { root, pages } = fixture()
|
||||
expect(rewriteMarkdown('[B](b.md)\n', {
|
||||
|
||||
Reference in New Issue
Block a user