← Files DataARCHIVED FILE
tests/publication-session.test.mjs
19.3 KB · Sep 30, 2026 · 23:19 UTC
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { lstat, mkdir, mkdtemp, readFile, realpath, rm, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { Readable, Writable } from "node:stream";
import test from "node:test";
import { createPublicationSession, servePublicationSession, asciiJson } from "../skills/publish-artifact-to-sites/scripts/publish-data-app.mjs";
import { PublicationGitError } from "../skills/publish-artifact-to-sites/scripts/publication-git.mjs";
import { publicationGitClient } from "../skills/publish-artifact-to-sites/scripts/publication-git-process.mjs";
const secret = "test-source-secret-that-must-never-be-printed";
const ingress = "test-ingress-secret-that-must-never-be-printed";
const sha = "a".repeat(40), manifestSha256 = "b".repeat(64);
const prepare = { op: "prepare", projectDir: "/authoring", projectId: "site-id",
publicationProjectDir: "/publication", archivePath: "/report.tar.gz", siteUrl: "https://reviewed.chatgpt.site" };
const deployment = { project_id: "site-id", id: "deployment-id", version_id: "version-id", status: "succeeded", url: prepare.siteUrl };
function fixture(changes = {}) {
const calls = [];
let tokenHash, uploadToken;
const source = { state: "pushed", commitSha: sha, manifestSha256 };
const session = createPublicationSession({
checkPreparation: async () => {},
preflightPublicationGit: async () => ({ projectDir: "/authoring", sourceRevision: sha, identity: { name: "Writer", email: "writer@example.test" } }),
initializePublicationGit: async () => { calls.push("initialize"); return { projectDir: "/authoring", sourceRevision: sha, identity: { name: "Writer", email: "writer@example.test" } }; },
packageDataAppForSites: async options => { calls.push("package"); tokenHash = options["deployment-token-sha256"]; },
createPublicationSource: async () => { calls.push("source"); return { publicationProjectDir: "/publication" }; },
publishPublicationSource: async options => { calls.push("push"); assert.equal(options.credential.token, secret); return source; },
verifyPublicationCandidate: async () => { calls.push("verify"); return { manifestSha256 }; },
createPublicationArchive: async () => { calls.push("archive"); return { archivePath: "/report.tar.gz", sha256: "c".repeat(64), bytes: 200, files: ["dist/server/index.js"] }; },
uploadDataAppAssets: async options => {
calls.push("upload"); uploadToken = options.deploymentToken;
assert.equal(options.sitesAuthorization, ingress);
return { ready: true, siteUrl: options.siteUrl, queryCount: 1, rowCount: 10, uploads: [], readback: {} };
}, ...changes,
});
return { session, calls, tokenHash: () => tokenHash, uploadToken: () => uploadToken };
}
test("native lifecycle boundary, one in-memory token, compact receipts and idempotent completed stages", async () => {
const f = fixture();
const responses = [];
responses.push(await f.session.dispatch({ op: "preflight" }));
responses.push(await f.session.dispatch(prepare));
assert.equal((await f.session.dispatch({ op: "upload", deployment })).code, "NOT_PUSHED");
const pushed = await f.session.dispatch({ op: "push", credential: { token: secret } });
responses.push(pushed);
assert.deepEqual(pushed.saveArguments, { project_id: "site-id", commit_sha: sha, archive: "/report.tar.gz" });
assert.equal(pushed.archive.fileCount, 1);
assert.equal(pushed.archive.files, undefined);
assert.equal((await f.session.dispatch({ op: "push" })).ok, true);
for (const invalid of [{ ...deployment, status: "pending" }, { ...deployment, project_id: "another-site" }]) {
assert.equal((await f.session.dispatch({ op: "upload", deployment: invalid })).code, "DEPLOYMENT_NOT_READY");
}
assert.equal((await f.session.dispatch({ op: "upload", deployment, siteUrl: "https://another.chatgpt.site", sitesAuthorization: ingress })).code, "INVALID_SITE");
responses.push(await f.session.dispatch({ op: "upload", deployment, siteUrl: prepare.siteUrl, sitesAuthorization: ingress }));
responses.push(await f.session.dispatch({ op: "upload" }));
assert.equal(responses.at(-1).stage, "ready");
const { createHash } = await import("node:crypto");
assert.equal(createHash("sha256").update(f.uploadToken()).digest("hex"), f.tokenHash());
assert.deepEqual(f.calls, ["package", "source", "push", "verify", "archive", "verify", "upload"]);
for (const forbidden of [secret, ingress, f.uploadToken(), "writer@example.test"]) assert.ok(!JSON.stringify(responses).includes(forbidden));
const closed = await f.session.dispatch({ op: "close" });
assert.equal(closed.projectId, undefined);
assert.equal((await f.session.dispatch({ op: "status" })).code, "SESSION_CLOSED");
});
test("credential retry retains exact Git receipt without packaging or creating source again", async () => {
const retry = { state: "committed", commitSha: sha, manifestSha256 };
let attempts = 0;
const f = fixture({ publishPublicationSource: async options => {
if (++attempts === 1) throw new PublicationGitError("NETWORK_FAILED", "push", retry);
assert.deepEqual(options.receipt, retry);
return { ...retry, state: "pushed" };
} });
await f.session.dispatch(prepare);
const failed = await f.session.dispatch({ op: "push", credential: { token: secret } });
assert.equal(failed.code, "NETWORK_FAILED");
assert.deepEqual(failed.source, retry);
assert.equal((await f.session.dispatch({ op: "push", credential: { token: secret } })).stage, "archived");
assert.equal(f.calls.filter(x => x === "package").length, 1);
assert.equal(f.calls.filter(x => x === "source").length, 1);
});
test("changed output during a push cannot be paired with the earlier source commit", async () => {
const f = fixture({ verifyPublicationCandidate: async () => ({ manifestSha256: "changed" }) });
await f.session.dispatch(prepare);
const result = await f.session.dispatch({ op: "push", credential: { token: secret } });
assert.equal(result.code, "SOURCE_CHANGED");
assert.equal(result.saveArguments, undefined);
assert.ok(!f.calls.includes("archive"));
});
test("unexpected failures never echo secrets; retries preserve the live upload token", async () => {
let attempts = 0;
const f = fixture({ uploadDataAppAssets: async options => {
if (++attempts === 1) throw new Error(`${secret} ${ingress} ${options.deploymentToken}`);
assert.ok(options.deploymentToken.length >= 32);
return { ready: true };
} });
await f.session.dispatch(prepare);
await f.session.dispatch({ op: "push", credential: { token: secret } });
const request = { op: "upload", deployment, siteUrl: prepare.siteUrl, sitesAuthorization: ingress };
const result = await f.session.dispatch(request);
assert.equal(result.code, "PUBLICATION_FAILED");
assert.ok(!JSON.stringify(result).includes(secret));
assert.ok(!JSON.stringify(result).includes(ingress));
assert.equal((await f.session.dispatch(request)).stage, "ready");
});
test("explicit new-project initialization uses its returned identity, without reading a parent again", async () => {
const f = fixture({ preflightPublicationGit: async () => { throw new Error("No Git config in new root"); } });
assert.equal((await f.session.dispatch({ ...prepare, initialize: true })).stage, "prepared");
assert.equal(f.calls[0], "initialize");
});
async function destinationFixture(t) {
const directory = await realpath(await mkdtemp(join(tmpdir(), "data publication destinations-")));
t.after(() => rm(directory, { recursive: true, force: true }));
const projectDir = join(directory, "authoring");
await mkdir(join(projectDir, "dist"), { recursive: true });
await writeFile(join(projectDir, "dist/data-app-build.json"), JSON.stringify({ kind: "separate-data-v1" }));
const calls = [];
const session = createPublicationSession({
preflightPublicationGit: async options => {
calls.push("preflight");
return { projectDir: options.projectDir, sourceRevision: sha, identity: { name: "Writer", email: "writer@example.test" } };
},
packageDataAppForSites: async () => { calls.push("package"); },
createPublicationSource: async options => {
calls.push("source");
await mkdir(options.publicationProjectDir);
return { publicationProjectDir: options.publicationProjectDir };
},
});
return { directory, projectDir, calls, session, request: { ...prepare, projectDir,
publicationProjectDir: join(directory, "output", "nested café 🚀", "checkout"),
archivePath: join(directory, "archives", "nested café 🚀", "report.tar.gz") } };
}
test("preparation safely creates fresh nested output parents and uses canonical paths", async t => {
const f = await destinationFixture(t);
const alias = join(f.directory, "output alias");
await symlink(f.directory, alias, process.platform === "win32" ? "junction" : "dir");
const result = await f.session.dispatch({ ...f.request,
publicationProjectDir: join(alias, "output", "nested café 🚀", "checkout") });
assert.equal(result.ok, true, JSON.stringify(result));
assert.equal(result.publicationProjectDir, f.request.publicationProjectDir);
assert.ok((await lstat(dirname(f.request.archivePath))).isDirectory());
await assert.rejects(lstat(f.request.archivePath), { code: "ENOENT" });
assert.deepEqual(f.calls, ["preflight", "package", "source"]);
});
test("an occupied destination fails specifically before creating parents or packaging", async t => {
const f = await destinationFixture(t);
const archivePath = join(f.directory, "existing.tar.gz");
await writeFile(archivePath, "keep existing archive");
const result = await f.session.dispatch({ ...f.request, archivePath });
assert.equal(result.code, "DESTINATION_EXISTS");
assert.equal(result.failedStage, "preparation");
assert.equal(await readFile(archivePath, "utf8"), "keep existing archive");
await assert.rejects(lstat(join(f.directory, "output")), { code: "ENOENT" });
assert.deepEqual(f.calls, []);
});
test("aliases into authoring and overlapping destinations are rejected before mutation", async t => {
const f = await destinationFixture(t);
const authoringAlias = join(f.directory, "authoring alias");
await symlink(f.projectDir, authoringAlias, process.platform === "win32" ? "junction" : "dir");
for (const changes of [
{ publicationProjectDir: join(authoringAlias, "fresh", "checkout") },
{ archivePath: join(f.request.publicationProjectDir, "fresh", "report.tar.gz") },
{ publicationProjectDir: join(f.request.archivePath, "fresh", "checkout") },
{ archivePath: f.request.publicationProjectDir },
]) {
assert.equal((await f.session.dispatch({ ...f.request, ...changes })).code, "INVALID_DESTINATION");
}
await assert.rejects(lstat(join(f.projectDir, "fresh")), { code: "ENOENT" });
await assert.rejects(lstat(join(f.directory, "output")), { code: "ENOENT" });
await assert.rejects(lstat(join(f.directory, "archives")), { code: "ENOENT" });
assert.deepEqual(f.calls, []);
});
test("invalid parent files, dangling aliases and unsupported artifacts cannot create output parents", async t => {
const f = await destinationFixture(t);
const file = join(f.directory, "file");
await writeFile(file, "keep existing file");
assert.equal((await f.session.dispatch({ ...f.request, archivePath: join(file, "report.tar.gz") })).code, "INVALID_DESTINATION");
const alias = join(f.directory, "dangling");
await symlink(join(f.directory, "missing"), alias, process.platform === "win32" ? "junction" : "dir");
assert.equal((await f.session.dispatch({ ...f.request, archivePath: alias })).code, "DESTINATION_EXISTS");
assert.equal((await f.session.dispatch({ ...f.request, archivePath: join(alias, "report.tar.gz") })).code, "INVALID_DESTINATION");
await writeFile(join(f.projectDir, "dist/data-app-build.json"), JSON.stringify({ kind: "legacy" }));
assert.equal((await f.session.dispatch(f.request)).code, "UNSUPPORTED_LAYOUT");
assert.equal(await readFile(file, "utf8"), "keep existing file");
await assert.rejects(lstat(join(f.directory, "output")), { code: "ENOENT" });
await assert.rejects(lstat(join(f.directory, "archives")), { code: "ENOENT" });
assert.deepEqual(f.calls, []);
});
test("failed receipts retain elapsed stage timing and whitelist Git diagnostics", async () => {
const error = new PublicationGitError("NETWORK_FAILED", "fetch");
Object.assign(error, { operation: "ls-remote", subtype: "connection_reset", operationMilliseconds: 12,
message: secret, stderr: ingress, command: secret, token: secret });
const f = fixture({ publishPublicationSource: async () => {
await new Promise(resolve => setTimeout(resolve, 20));
throw error;
} });
await f.session.dispatch(prepare);
const result = await f.session.dispatch({ op: "push", credential: { token: secret } });
assert.equal(result.failedStage, "push");
assert.equal(result.gitStage, "fetch");
assert.equal(result.operation, "ls-remote");
assert.equal(result.subtype, "connection_reset");
assert.equal(result.operationMilliseconds, 12);
assert.ok(result.milliseconds >= 10);
assert.ok(result.stageMilliseconds >= 10);
assert.ok(result.milliseconds >= result.stageMilliseconds);
assert.ok(!JSON.stringify(result).includes(secret));
assert.ok(!JSON.stringify(result).includes(ingress));
Object.assign(error, { operation: secret, subtype: secret, stage: secret, operationMilliseconds: Infinity });
const invalid = await f.session.dispatch({ op: "push", credential: { token: secret } });
for (const field of ["gitStage", "operation", "subtype", "operationMilliseconds"]) assert.equal(invalid[field], undefined);
assert.ok(!JSON.stringify(invalid).includes(secret));
});
test("unexpected errors cannot manufacture public codes or messages; archive stage timing stays separate", async () => {
const f = fixture({ createPublicationArchive: async () => {
throw Object.assign(new Error(secret), { publicationCode: secret, code: secret, operation: secret, subtype: secret });
} });
await f.session.dispatch(prepare);
const result = await f.session.dispatch({ op: "push", credential: { token: secret } });
assert.equal(result.code, "PUBLICATION_FAILED");
assert.equal(result.stage, "pushed");
assert.equal(result.failedStage, "archive");
assert.ok(Number.isSafeInteger(result.milliseconds));
assert.ok(Number.isSafeInteger(result.stageMilliseconds));
assert.ok(!JSON.stringify(result).includes(secret));
});
test("session emits only trusted Git stderr excerpts and redacts credentials despite hostile error fields", async () => {
const git = publicationGitClient({ projectDir: "/publication", runner: () => ({ status: 128, signal: null,
stderr: `fatal: source service refused the connection\nAuthorization: Bearer ${secret}\nproxy-authorization: Basic ${Buffer.from(ingress).toString("base64")}\n`,
stdout: secret }) });
let error;
try { git(["ls-remote", "https://reviewed.chatgpt.site"], { stage: "fetch", fallback: "NETWORK_FAILED",
config: [["http.extraHeader", `Authorization: Bearer ${secret}`]] }); }
catch (caught) { error = caught; }
assert.ok(error instanceof PublicationGitError);
Object.assign(error, { stderrExcerpt: secret, stderr: secret, stdout: ingress, exitStatus: secret, signal: secret });
const f = fixture({ publishPublicationSource: async () => { throw error; } });
await f.session.dispatch(prepare);
const result = await f.session.dispatch({ op: "push", credential: { token: secret } });
assert.equal(result.exitStatus, 128);
assert.equal(result.signal, null);
assert.match(result.stderrExcerpt, /source service refused the connection/u);
for (const value of [secret, ingress, Buffer.from(ingress).toString("base64")]) assert.ok(!JSON.stringify(result).includes(value));
error = Object.assign(new PublicationGitError("NETWORK_FAILED", "fetch"), {
stderrExcerpt: secret, exitStatus: 128, signal: "SIGTERM",
});
const forged = await f.session.dispatch({ op: "push", credential: { token: secret } });
for (const field of ["stderrExcerpt", "exitStatus", "signal"]) assert.equal(forged[field], undefined);
assert.ok(!JSON.stringify(forged).includes(secret));
});
test("JSONL accepts fragmented CRLF, CR, Unicode and EOF; output is ASCII and requests are not echoed", async () => {
const values = [], output = [];
const session = { dispatch: async request => { values.push(request); return request.op === "close"
? { ok: true, stage: "closed" } : { ok: true, stage: "echo-test", path: request.path }; } };
const path = "C:\\Reports and notes\\café 🚀\\report.html";
const input = Buffer.from(`${JSON.stringify({ path })}\r\n{bad-${secret}}\r${JSON.stringify({ path })}`);
await servePublicationSession(Readable.from(Array.from(input, byte => Buffer.from([byte]))), new Writable({ write(chunk, _, callback) { output.push(chunk); callback(); } }), session);
const text = Buffer.concat(output).toString("utf8");
assert.match(text, /^[\x00-\x7f]*$/u);
assert.ok(!text.includes(secret));
const replies = text.trim().split("\n").map(JSON.parse);
assert.equal(replies[0].echo, false);
assert.equal(replies[1].path, path);
assert.equal(replies[2].code, "INVALID_JSON");
assert.equal(replies[3].path, path);
assert.equal(values.at(-1).op, "close");
assert.equal(JSON.parse(asciiJson({ path })).path, path);
});
test("oversized and invalid UTF-8 commands are rejected without retaining or printing input", async () => {
const output = [], dispatched = [];
await servePublicationSession(Readable.from([Buffer.alloc(65537, 120), Buffer.from("\n"), Buffer.from([0xff, 10]), Buffer.from('{"op":"close"}\n')]),
new Writable({ write(chunk, _, callback) { output.push(chunk); callback(); } }),
{ dispatch: async request => { dispatched.push(request); return { ok: true, stage: "closed" }; } });
const text = Buffer.concat(output).toString();
assert.ok(text.length < 1000);
assert.ok(text.includes("REQUEST_TOO_LARGE"));
assert.ok(text.includes("INVALID_JSON"));
assert.ok(dispatched.every(x => x.op === "close"));
});
test("CLI starts through an aliased directory and remains importable from stdin", async t => {
const directory = await mkdtemp(join(tmpdir(), "data publication CLI-"));
t.after(() => rm(directory, { recursive: true, force: true }));
const scriptDirectory = fileURLToPath(new URL("../skills/publish-artifact-to-sites/scripts/", import.meta.url));
const alias = join(directory, "publishing café 🚀");
await symlink(scriptDirectory, alias, process.platform === "win32" ? "junction" : "dir");
const result = spawnSync(process.execPath, [join(alias, "publish-data-app.mjs")], {
input: '{"op":"status"}\r\n{"op":"close"}\r\n', encoding: "utf8",
});
assert.equal(result.status, 0, result.stderr);
assert.equal(result.stderr, "");
assert.deepEqual(result.stdout.trim().split("\n").map(line => JSON.parse(line).stage), ["listening", "idle", "closed"]);
const moduleUrl = new URL("../skills/publish-artifact-to-sites/scripts/publish-data-app.mjs", import.meta.url).href;
for (const stdinArgument of [[], ["-"]]) {
const imported = spawnSync(process.execPath, ["--input-type=module", ...stdinArgument], {
input: `const { createPublicationSession } = await import(${JSON.stringify(moduleUrl)});\n` +
'console.log((await createPublicationSession().dispatch({op:"status"})).stage);\n', encoding: "utf8",
});
assert.equal(imported.status, 0, imported.stderr);
assert.equal(imported.stdout, "idle\n");
}
});
SHA-256: d1fad4f035b49d9a39ad6aa7572a1387a8771df6257943f7fc592e20ac603cc0