Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157// The ziran CLI, end to end as a real subprocess against the fake network:// login/logout/accounts/use, publish from .tile AND from unpacked sources,// list, version chain, unpublish.// Run: deno run -A src/cli/cli.test.ts
import { join } from 'jsr:@std/path@1';import { FakeSpacesNetwork } from '../sync/spaces/fake.ts';import { getPublished } from '../store/store.ts';import { writeTile } from '../server/tilefile.ts';
let failures = 0;const check = (label: string, ok: boolean, detail = '') => { console.log(`${ok ? 'PASS' : 'FAIL'} ${label}${detail ? ` — ${detail}` : ''}`); if (!ok) failures++;};
const net = new FakeSpacesNetwork();const alice = net.account('alice');const bob = net.account('bob');
const dir = await Deno.makeTempDir({ prefix: 'ziran-cli-' });const configDir = join(dir, 'config');
// A packed tile on disk.const tilePath = join(dir, 'My Notes.tile');await writeTile( { name: 'Untitled notes', description: 'Notes for testing.', model: { id: 'com.example.notes', name: 'Notes' }, resources: {}, }, new Map([['/', new TextEncoder().encode('<!doctype html><title>n</title>')]]), tilePath,);
// An UNPACKED tile: a directory with manifest.json + index.html + an asset.const unpacked = join(dir, 'Party Game');await Deno.mkdir(unpacked);await Deno.writeTextFile( join(unpacked, 'manifest.json'), JSON.stringify({ name: 'Untitled party', description: 'A game.', resources: {} }),);await Deno.writeTextFile(join(unpacked, 'index.html'), '<!doctype html><title>p</title>');await Deno.writeTextFile(join(unpacked, 'style.css'), 'body { margin: 0 }');
const CLI = new URL('./ziran.ts', import.meta.url).pathname;async function ziran(args: string[], env: Record<string, string> = {}): Promise<{ code: number; out: string; err: string }> { const cmd = await new Deno.Command(Deno.execPath(), { args: ['run', '-A', CLI, ...args], // ZIRAN_NO_ENV_CREDS: the test must NEVER fall through to ambient // credentials (a real .env.store-publisher sits at the repo root). env: { ZIRAN_CONFIG_DIR: configDir, ZIRAN_NO_ENV_CREDS: '1', ...env }, stdout: 'piped', stderr: 'piped', }).output(); return { code: cmd.code, out: new TextDecoder().decode(cmd.stdout), err: new TextDecoder().decode(cmd.stderr), };}// The legacy-chain test provides a COMPLETE env identity (so no file is// ever consulted) and re-enables the chain.const legacyEnv = { ZIRAN_HANDLE: alice.handle, ZIRAN_PASSWORD: alice.password, ZIRAN_PDS: net.url, ZIRAN_NO_ENV_CREDS: '',};
/* ——— help & refusals ——— */
const help = await ziran(['help']);check('help prints usage', help.code === 0 && help.out.includes('publish <source>'), help.err);const unknown = await ziran(['frobnicate']);check('an unknown command is refused with guidance', unknown.code === 1 && unknown.err.includes('ziran help'));const noone = await ziran(['publish', tilePath]);check('publishing signed out says how to sign in', noone.code === 1 && noone.err.includes('ziran login'), noone.err);
/* ——— login / accounts / use / logout ——— */
const badPw = await ziran(['login', alice.handle, '--password', 'wrong', '--pds', net.url]);check('login verifies before storing (bad password fails)', badPw.code === 1, badPw.out);const inA = await ziran(['login', alice.handle, '--password', alice.password, '--pds', net.url]);check('login signs in and stores', inA.code === 0 && inA.out.includes(`signed in as ${alice.handle}`), inA.err);check('the first login is the default', inA.out.includes('default'));await ziran(['login', bob.handle, '--password', bob.password, '--pds', net.url]);const who = await ziran(['accounts']);check( 'accounts lists everyone, * marks the default', who.out.includes(alice.handle) && who.out.includes(bob.handle) && who.out.split('\n').some((l) => l.startsWith('*') && l.includes(alice.handle)), who.out,);const mode = (await Deno.stat(join(configDir, 'accounts.json'))).mode;check('the accounts file is owner-only', mode !== null && (mode & 0o077) === 0, String(mode! & 0o777));
/* ——— publish: default account, --as, sources ——— */
const pub = await ziran(['publish', tilePath]);check('publish uses the default account', pub.code === 0 && pub.out.includes(`@${alice.handle}/my-notes`), pub.err);check('…reports the version cid', /version: baf/.test(pub.out), pub.out);check('the record is really on the server', (await getPublished(fetch, net.url, alice.did, 'my-notes'))?.value.title === 'Notes');
const asBob = await ziran(['publish', tilePath, '--as', bob.handle, '--name', 'notes-b']);check('--as picks another signed-in account', asBob.code === 0 && asBob.out.includes(`@${bob.handle}/notes-b`), asBob.err);check('…and lands in THEIR repo', (await getPublished(fetch, net.url, bob.did, 'notes-b')) !== null);
const fromDir = await ziran(['publish', unpacked]);check('publish packs an unpacked-tile directory', fromDir.code === 0 && fromDir.out.includes(`@${alice.handle}/party-game`), fromDir.err);const dirPub = await getPublished(fetch, net.url, alice.did, 'party-game');check('…as an app (no model field)', dirPub?.value.kind === 'app');check('…with its resources aboard', Boolean(dirPub?.value.tile.resources['/style.css']), JSON.stringify(Object.keys(dirPub?.value.tile.resources ?? {})));
const fromManifest = await ziran(['publish', join(unpacked, 'manifest.json'), '--name', 'party-two']);check('a manifest.json path publishes its directory', fromManifest.code === 0 && fromManifest.out.includes('party-two'), fromManifest.err);
/* ——— use, list, legacy env, version chain, unpublish ——— */
await ziran(['use', bob.handle]);const who2 = await ziran(['accounts']);check('use switches the default', who2.out.split('\n').some((l) => l.startsWith('*') && l.includes(bob.handle)), who2.out);
const listed = await ziran(['list', alice.did, '--pds', net.url]);check('list shows publishes', listed.code === 0 && listed.out.includes('my-notes'), listed.out || listed.err);
const legacy = await ziran(['publish', tilePath, '--name', 'legacy-envs'], { ...legacyEnv, ZIRAN_CONFIG_DIR: join(dir, 'empty-config') });check('the env credential chain still works when nobody is signed in', legacy.code === 0 && legacy.out.includes(alice.handle), legacy.err);
await writeTile( { name: 'Untitled notes', description: 'Better notes.', model: { id: 'com.example.notes', name: 'Notes' }, resources: {}, }, new Map([['/', new TextEncoder().encode('<!doctype html><title>n2</title>')]]), tilePath,);const pub2 = await ziran(['publish', tilePath, '--name', 'my-notes', '--as', alice.handle]);check('re-publishing a changed tile chains a version', pub2.code === 0 && pub2.out.includes('replaces baf'), pub2.out || pub2.err);
const gone = await ziran(['unpublish', 'my-notes', '--as', alice.handle]);check('unpublish succeeds', gone.code === 0, gone.err);check('the record is really gone', (await getPublished(fetch, net.url, alice.did, 'my-notes')) === null);
const outB = await ziran(['logout', bob.handle]);check('logout removes the account and moves the default', outB.code === 0 && outB.out.includes(`default is now ${alice.handle}`), outB.out);const who3 = await ziran(['accounts']);check('the signed-out account is gone', !who3.out.includes(bob.handle), who3.out);
await Deno.remove(dir, { recursive: true });await net.close();console.log(failures ? `\n${failures} FAILED` : '\nall green');Deno.exit(failures ? 1 : 0);