Files
AgentHub/server/__tests__/artifact-export-hint.test.js
T
nntrivi2001 c946b73d11 chore: v2.8.71 — remote clients (company hub), artifacts shared with chosen people, folders and viewable copies, fixes
Remote clients (docs/plans/remote-clients/README.md, B1–B3 for the Claude pool):
- server/hub/ holds the hub-only code (devices, remote sessions, upload ingest,
  LAN gateway), required only in server mode; hub-boundary.test.js keeps local
  code from importing it.
- A local install enrolls with `agenthub remote login` or Settings → AI tools →
  Company hub; the "Company account" AI Proxy (reserved name agenthub.company)
  sends a session through the hub's gateway, which adds a pool account's token —
  no credential reaches the machine. Only those sessions are uploaded, hourly
  and at their end; secret-shaped strings are redacted on the client.
- Gateway refuses: unknown device (401), a message without a session id, an
  unregistered session under AGENTHUB_REMOTE_GATE=block, a user over quota
  (EQUOTA), a purged session (410). Device tokens 90 days, conversations 30.
- New alert rule type remote_unmonitored: a company-account session its device
  never registered; a late registration clears the flag instead.
- The hub imports a session once per upload round (chunks carry `more`),
  not once per chunk.

Artifacts:
- Share an artifact with chosen people (artifact_shares, PATCH sharedWith,
  GET /api/artifacts/people) besides everyone; a "Shared with you" tab.
  Only the owner or an admin sees or edits who it is shared with; audited.
- Folders: each account's own arrangement (agenthub.artifacts.folders pref).
- A claude.ai page keeps a copy viewable without the publishing account: its
  HTML file (and supporting files) snapshotted at publish, or the copy Claude
  saves for a Doc / Slides / Design in artifacts-export/ (a synchronous
  PostToolUse hint hook, scripts/artifact-export-hint.js, admin switch),
  attached to the page at Stop. Viewed in HtmlFrame's sandbox, Markdown, or
  pdf.js (pdfjs-dist 6.4.299, legacy build, loaded only for a PDF).

Fixes:
- tmux: a cached /proc/<pid>/exe of an exited server falls back to PATH's
  tmux (stop the last pane, start again within 10 s failed with ENOENT).
- shelves: a shelf restores beside changes in other files.
- chat: a finished reply ends the turn even with no hook at all.
- Settings → AI tools: profile list display tweaks.
2026-11-04 15:16:49 +07:00

67 lines
2.8 KiB
JavaScript

/**
* @file Tests for scripts/artifact-export-hint.js: when Claude is asked to save
* a viewable copy of a published Doc / typed artifact, and when it is not.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { describe, it } = require("node:test");
const assert = require("node:assert/strict");
const { spawnSync } = require("node:child_process");
const path = require("node:path");
const { hintFor } = require("../../scripts/artifact-export-hint");
const PANE = { AGENTHUB_LANE_ID: "3" };
const docBirth = {
tool_name: "mcp__claude_ai_Claude_Docs__batch",
tool_input: { container: { kind: "project", create: { name: "Plan" } } },
tool_response: [{ type: "text", text: '{"frame":{"url":"https://claude.ai/artifact/doc-9"}}' }],
};
const slides = {
tool_name: "Artifact",
tool_input: { type_url: "https://claude.ai/types/slides", title: "Deck" },
tool_response: "Created https://claude.ai/artifact/deck-1",
};
describe("artifact export hint", () => {
it("asks for a Markdown copy of a new doc and a PDF/HTML copy of a typed artifact", () => {
assert.match(hintFor(docBirth, PANE), /artifacts-export\/doc-9\.md/);
const s = hintFor(slides, PANE);
assert.match(s, /artifacts-export\/deck-1\.pdf/);
assert.match(s, /artifacts-export\/deck-1\.html/);
});
it("stays silent everywhere else", () => {
assert.equal(hintFor(slides, {}), null, "not an AgentHub pane");
assert.equal(hintFor(slides, { ...PANE, AGENTHUB_ARTIFACT_EXPORT: "0" }), null, "turned off");
const html = { ...slides, tool_input: { file_path: "/w/page.html" } };
assert.equal(hintFor(html, PANE), null, "an HTML page is copied by AgentHub itself");
const edit = {
...docBirth,
tool_input: { container: { kind: "project", id: "p1" }, batch: [] },
};
assert.equal(hintFor(edit, PANE), null, "a later batch edits the doc");
assert.equal(hintFor({ ...slides, tool_response: "no link" }, PANE), null);
assert.equal(hintFor({ ...slides, tool_name: "ArtifactData" }, PANE), null);
assert.equal(hintFor(null, PANE), null);
});
it("prints Claude Code's PostToolUse context, and nothing on bad input", () => {
const script = path.join(__dirname, "..", "..", "scripts", "artifact-export-hint.js");
const run = (input, env = PANE) =>
spawnSync(process.execPath, [script], {
input,
env: { PATH: process.env.PATH, ...env },
encoding: "utf8",
timeout: 10_000,
});
const ok = run(JSON.stringify(slides));
assert.equal(ok.status, 0);
const out = JSON.parse(ok.stdout);
assert.equal(out.hookSpecificOutput.hookEventName, "PostToolUse");
assert.match(out.hookSpecificOutput.additionalContext, /deck-1/);
const bad = run("{not json");
assert.equal(bad.status, 0);
assert.equal(bad.stdout, "");
});
});