Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,7 @@ async function checkT3Support(server: LocalT3): Promise<void> {
);
supported = stdout.trim() === "true";
} else
supported = readFileSync(server.cli, "utf8").includes(
"T3CODE_CODEX_LAUNCH_ARGS",
);
supported = readFileSync(server.cli).includes("T3CODE_CODEX_LAUNCH_ARGS");
if (!supported)
throw new Error(
"This T3 build could not be verified to support Codex launch arguments. Update T3 before T3-only setup.",
Expand Down
23 changes: 21 additions & 2 deletions src/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type LocalT3 = {
origin: string;
node: string;
cli: string;
native?: boolean;
electron?: boolean;
};
const renewalWindow = 24 * 60 * 60 * 1000;
Expand Down Expand Up @@ -105,6 +106,23 @@ export function inspectLocal(baseDir: string): LocalT3 | undefined {
// process has this home's database open before running its auth CLI.
if (!hasOpenFile(state.pid, join(baseDir, "userdata/state.sqlite")))
return;
} else if (args[1] === "serve") {
// Native npm distributions run `t3 serve`, with no JS entrypoint.
// Verify the live executable against its platform package before using
// that same executable to issue or revoke credentials.
const pkg = JSON.parse(
readFileSync(join(dirname(executable), "package.json"), "utf8"),
);
// Setup's Node may run under emulation while T3 uses the host CPU.
if (
!["x64", "arm64"].some(
(arch) => pkg.name === `@t3code/t3-${process.platform}-${arch}`,
)
)
return;
const binary = process.platform === "win32" ? "t3.exe" : "t3";
if (executable !== join(dirname(executable), binary)) return;
cli = executable;
} else {
cli = realpathSync(resolve(cwd, args[1]));
if (!cli.endsWith(join("dist", "bin.mjs"))) return;
Expand Down Expand Up @@ -141,6 +159,7 @@ export function inspectLocal(baseDir: string): LocalT3 | undefined {
node: executable,
cli,
...(electron ? { electron: true } : {}),
...(args[1] === "serve" ? { native: true } : {}),
};
} catch {
return;
Expand Down Expand Up @@ -237,7 +256,7 @@ async function issue(
await exec(
server.node,
[
server.cli,
...(server.native ? [] : [server.cli]),
"auth",
"session",
"revoke",
Expand All @@ -260,7 +279,7 @@ async function issue(
const { stdout } = await exec(
server.node,
[
server.cli,
...(server.native ? [] : [server.cli]),
"auth",
"session",
"issue",
Expand Down
107 changes: 100 additions & 7 deletions tests/setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { Service } from "../src/service.js";
const exec = promisify(execFile);
async function fixture(
t: { after: (fn: () => Promise<void>) => void },
launch: "direct" | "absolute-link" | "relative-link" = "direct",
launch: "direct" | "absolute-link" | "relative-link" | "native" = "direct",
) {
const root = mkdtempSync(join(tmpdir(), "t3poll setup café space-"));
const base = join(root, "t3");
Expand All @@ -44,7 +44,7 @@ async function fixture(
const cli = join(pkg, "dist/bin.mjs");
copyFileSync(resolve("tests/fixtures/local-t3.mjs"), cli);
const link = join(root, "t3-bin");
if (launch !== "direct") {
if (launch !== "direct" && launch !== "native") {
if (process.platform === "win32") symlinkSync(pkg, link, "junction");
else symlinkSync(cli, link);
}
Expand All @@ -54,11 +54,34 @@ async function fixture(
process.platform === "win32" && launch !== "direct"
? join(command, "dist", "bin.mjs")
: command;
const child = spawn(process.execPath, [entry, "serve"], {
cwd: root,
env: { ...process.env, T3CODE_HOME: base },
stdio: ["ignore", "pipe", "pipe"],
});
let executable = process.execPath;
if (launch === "native") {
// A copied Node binary gives us a real process with the native `t3 serve`
// argv layout, without requiring a T3 download or compiler in CI.
executable = join(pkg, process.platform === "win32" ? "t3.exe" : "t3");
copyFileSync(process.execPath, executable);
writeFileSync(
join(pkg, "package.json"),
JSON.stringify({
name: `@t3code/t3-${process.platform}-${process.arch}`,
}),
);
writeFileSync(join(root, "package.json"), '{"type":"module"}');
copyFileSync(cli, join(root, "serve"));
writeFileSync(
join(root, "auth"),
'process.argv.splice(1, 0, "auth");\n' + readFileSync(cli, "utf8"),
);
}
const child = spawn(
executable,
launch === "native" ? ["serve"] : [entry, "serve"],
{
cwd: root,
env: { ...process.env, T3CODE_HOME: base },
stdio: ["ignore", "pipe", "pipe"],
},
);
t.after(async () => {
if (child.exitCode === null) {
const stopped = once(child, "exit");
Expand Down Expand Up @@ -431,3 +454,73 @@ test("installer restores configs when credential verification fails", async (t)
codexBinary,
);
});

test("native T3 discovery, issuance, renewal, and failed-verification cleanup", async (t) => {
const f = await fixture(t, "native");
assert.equal(discover(f.config).origin, f.origin);
// The stand-in binary loads the auth fixture from its working directory.
const cwd = process.cwd();
process.chdir(f.root);
try {
const c = await connection(f.config);
assert.deepEqual(await new T3(c.origin, c.tokenFile).threads(), []);
assert.equal(f.issued(), 1);
const path = `${c.tokenFile}.managed.json`;
const metadata = JSON.parse(readFileSync(path, "utf8"));
metadata.expiresAt = new Date(0).toISOString();
writeFileSync(path, JSON.stringify(metadata));
writeFileSync(join(f.base, "reject"), "");
await assert.rejects(renewManaged(c.tokenFile, c.origin), /preserved/);
assert.equal(
readFileSync(join(f.base, "revoked"), "utf8").trim().split("\n").length,
1,
);
rmSync(join(f.base, "reject"));
await renewManaged(c.tokenFile, c.origin);
assert.equal(f.issued(), 3);
assert.deepEqual(await new T3(c.origin, c.tokenFile).threads(), []);
} finally {
process.chdir(cwd);
}
});

test("native discovery rejects unrelated packages and mismatched homes", async (t) => {
const f = await fixture(t, "native");
const other = join(f.root, "other");
mkdirSync(join(other, "userdata"), { recursive: true });
copyFileSync(
join(f.base, "userdata/server-runtime.json"),
join(other, "userdata/server-runtime.json"),
);
assert.equal(inspectLocal(other), undefined);
assert.ok(inspectLocal(f.base));
writeFileSync(join(f.root, "package/package.json"), '{"name":"unrelated"}');
assert.equal(inspectLocal(f.base), undefined);
assert.equal(f.issued(), 0);
});

test("native discovery accepts a supported package architecture independently of setup's Node architecture", async (t) => {
const f = await fixture(t, "native");
const packagePath = join(f.root, "package/package.json");
// The stand-in executable stays the same; only the target package identity
// changes, reproducing a setup runtime and T3 with different architectures.
for (const arch of ["x64", "arm64"]) {
writeFileSync(
packagePath,
JSON.stringify({
name: `@t3code/t3-${process.platform}-${arch}`,
}),
);
assert.equal(inspectLocal(f.base)?.native, true, `${arch} package`);
}
const otherPlatform = process.platform === "darwin" ? "linux" : "darwin";
for (const name of [
`@t3code/t3-${process.platform}-ia32`,
`@t3code/t3-${process.platform}-x64-extra`,
`@t3code/t3-${otherPlatform}-arm64`,
]) {
writeFileSync(packagePath, JSON.stringify({ name }));
assert.equal(inspectLocal(f.base), undefined, name);
}
assert.equal(f.issued(), 0);
});
Loading